{
	"id": "1f3a67b7-33f5-4dea-bd8b-3e62685fb639",
	"created_at": "2026-04-06T00:06:19.828914Z",
	"updated_at": "2026-04-10T13:12:14.753594Z",
	"deleted_at": null,
	"sha1_hash": "604d26a9283e1d6a33464610b9b2b4c25e4989ce",
	"title": "Exploit, steganography and Delphi: unpacking DBatLoader",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 2249441,
	"plain_text": "Exploit, steganography and Delphi: unpacking DBatLoader\r\nBy Malcat EI\r\nArchived: 2026-04-05 18:35:51 UTC\r\nSample:\r\n13063a496da7e490f35ebb4f24a138db4551d48a1d82c0c876906a03b8e83e05 (Bazaar, VT)\r\nInfection chain:\r\nExcel stylesheet -\u003e Office equation -\u003e Shellcode (downloader) -\u003e DBatLoader stage 1 (stegano dropper) -\u003e\r\nDBatLoader stage 2 (discord downloader) -\u003e DBatLoader stage 3 (resource dropper) -\u003e Stone packed -\u003e Formbook\r\nTools used:\r\nMalcat, Speakeasy emulator, Hex Rays\r\nDifficulty:\r\nIntermediate\r\nIntroduction\r\nIf you are doing cyber threat research on the internet, chances are you will find a ton of papers documenting malicious\r\nRATs, APTs and state-sponsored campaigns. It is indeed interesting (and it makes cyber security folks feel like James Bond),\r\nbut sadly little attention is given to what makes most of the threat landscape: the packers, droppers and other downloaders at\r\nthe front of the infection chain. They may be less sophisticated, but it is what the user first encounters, and what makes most\r\nof the threat landscape.\r\nThe truth is, if an antivirus successfully detects and blocks an advanced RAT on a system, it means that it already failed and\r\nthat the system is compromised, because advanced RAT are at the end of the infection chain.\r\nTo illustrate our point, we will inspect a Formbook sample and we won't talk about Formbook at all. Instead we will dissect\r\nthe infection chain which leads to the installation of Formbook. As you will see, it is actually more complex than one might\r\nthink.\r\nExploiting CVE-2018-0798\r\nExcel document\r\nThe malware we are analyzing today is an encrypted OpenXML Excel document that came as email attachment. OpenXML\r\ndocuments are usually just ZIP archives containing XML files and are easy to analyze, but not encrypted documents like this\r\none. In fact, when a user chose to protect its Excel sheet, Microsoft Excel will encrypt it (using the magical password\r\nVelvetSweatshop ) and store it inside an OLE container. And when the user opens the document, Office will transparently\r\ndecrypt it without any user interaction. Malware authors are well aware of that fact and tend to abuse Excel encryption in\r\norder to evade antivirus detection. Fortunately, this is an old technique and tools exist to decrypt this kind of files. In fact, it\r\nis as simple as a few lines of python:\r\nimport msoffcrypto\r\n\"\"\"\r\nNOTE: for this script to work, you will have to install msoffcrypto: pip3 install msoffcrypto-tool\r\n\"\"\"\r\nwith open(\"13063a496da7e490f35ebb4f24a138db4551d48a1d82c0c876906a03b8e83e05.xlsx\", \"rb\") as f_in:\r\n doc = msoffcrypto.OfficeFile(f_in)\r\n doc.load_key(password=\"VelvetSweatshop\")\r\n with open(\"file0_stage0.xlsx.dec\", \"wb\") as f_out:\r\n doc.decrypt(f_out)\r\nThis gives us an OpenXML ZIP archive. Browsing the content, we can see a few things worth of interest:\r\nhttps://malcat.fr/blog/exploit-steganography-and-delphi-unpacking-dbatloader/\r\nPage 1 of 16\n\nthe document contains pictures baiting the user to deactivate safe mode (see screenshot below)\r\nthere is no vbaProject.bin file in the archive, meaning no VBA macro\r\nthere is no Excel macro sheet\r\nthere are two embedded objects:\r\na Word document at xl/embeddings/Microsoft_Office_Word_Macro-Enabled_Document1.docm\r\nan OLE container at xl/embeddings/oleObject1.bin\r\nBeside these elements, the document looks pretty clean. The Word document only contains a single picture, but the OLE\r\ncontainer seems promising since its doctype GUID is 0002CE02-0000-0000-C000-000000000046 (Microsoft Equation 3.0\r\nobject). Equation objects have seen several vulnerabilities in the past years and are actively exploited in the wild. Let us dive\r\nin.\r\nFigure 1: Excel sheet baiting the user to deactivate safe mode\r\nBuggy equation\r\nIf we open the oLE10NATive stream of the OLE container xl/embeddings/oleObject1.bin inside Malcat, we can see a\r\nvery bare bone Equation 3.0 object which has been stripped to the minimal, leaving just enough to target the exploit. But\r\nwhich exploit? VirusTotal tends to detect it as CVE-2017-11882, but not all engines agree. Let us have a look at the data:\r\nFigure 2: Embedded OLE object\r\nUsing the documentation of the MTEF format found here, we can make sense of most of the stream:\r\nhttps://malcat.fr/blog/exploit-steganography-and-delphi-unpacking-dbatloader/\r\nPage 2 of 16\n\nOffset Size Meaning\r\n00 4\r\nThe OLE1 header specifying the size of the data in the stream. Office seems to ignore this value and\r\nuse the stream size from the OLE container instead\r\n04 5\r\nMTEF header. Only the MTEF version (3) and MTEF product(1 = Equation Editor) seem to have\r\nvalid values. The rest is most likely ignored by Office and has been randomized.\r\n09 2 First MTEF record: 0x0A = FULL SIZE record\r\n0B 6-? Second MTEF record: 0x05 = MATRIX record\r\nThe MATRIX record seem to be the culprit there, and it would mean that we are facing CVE-2018-0798. CVE-2018-0798 is\r\nsometimes confused with CVE-2018-0802 since Microsoft originally allocated the same CVE for two different\r\nvulnerabilities. But it is quite different from CVE-2017-11882 which exploits the FONT record: funny how most antivirus\r\ngot it wrong.\r\nAccording to this document, the MATRIX record triggers the exploit by setting the field NumberOfRows too high. Only 8\r\nbytes are reserved in eqnedt32.exe for the array RowPartitionLineTypes , but (2 * 0xec + 9) / 8 = 0x3c bytes are copied\r\ninstead, leading to a stack overflow:\r\nFigure 3: The equation object explained\r\nKnowing this, we can now start looking for a shellcode.\r\nThe shellcode\r\nBy quickly inspecting what follows the MATRIX record (so starting at offset 0x4D), we notice that offset 0x50 looks like\r\nthe start of a shellcode. Indeed, the push/pop/jmp chain tends to indicate a meterpreter-generated shellcode.\r\nhttps://malcat.fr/blog/exploit-steganography-and-delphi-unpacking-dbatloader/\r\nPage 3 of 16\n\nFigure 4: meterpreter-generated shellcode are easy to spot\r\nJudging by the high entropy of the rest of the stream, the shellcode is most likely encrypted. We could of course reverse it,\r\nbut it is faster to emulate the code. We will use the Speakeasy emulator from FireEye on the content of the oLE10NATive\r\nstream. You can use the following script:\r\nimport speakeasy\r\nimport speakeasy.winenv.arch as e_arch\r\nunpacker = speakeasy.Speakeasy()\r\nwith open(\"olenative10_stream.bin\", \"rb\") as ole10native:\r\n data = ole10native.read()\r\naddress = unpacker.load_shellcode(\"\", e_arch.ARCH_X86, data=data)\r\nunpacker.run_shellcode(address, 0x50) # shellcode starts at offset 0x50\r\nwith open(\"shellcode_decrypted.bin\", \"wb\") as f:\r\n f.write(unpacker.mem_read(address, len(data)))\r\nIf you are using Malcat, you can alternatively force a function declaration at offset 0x50 (start of the shellcode) and then run\r\nthe script speakeasy_shellcode.py . The shellcode gets decrypted and strings are now in plain text:\r\nFigure 5: Decrypting the shellcode with speakeasy\r\nhttps://malcat.fr/blog/exploit-steganography-and-delphi-unpacking-dbatloader/\r\nPage 4 of 16\n\nNo need to analyze the shellcode in depth. Judging by the strings, it is a simple downloader that fetches and runs a file from\r\nthe url hxxp://104.168.32.50/009/vbc.exe (still online at time of writing). So let us fetch the data and move on.\r\nFirst stage: a bit of steganography\r\nThe file vbc.exe is a 937KB Delphi application of sha256\r\n3045902d7104e67ca88ca54360d9ef5bfe5bec8b575580bc28205ca67eeba96d (Bazaar, VT). Because of its size, reversing the\r\ncomplete application is out of question. We could send it to a sandbox, but our goal is to analyze and understand the\r\ndropper. So let us try to locate the payload instead by looking at anomalies.\r\nLocating the payload\r\nSweeping quickly through the binary, we find two points of interest:\r\nA huge string (104427 bytes) at address 0x0046f718\r\nA resource bitmap named BBTREX which does not look like the standard one (size is different, resource language\r\ntoo). Visually, the resource is a picture and definitely not an icon like the rest. It has most likely been patched post-compilation.\r\nFigure 6: Weird bitmap resource BBTREX\r\nThese two objects are referenced by the same function at offset 0x46D330 , which is quite convenient. This function is\r\nlocated near the end of the CODE section, which is also of importance. Delphi application are structured in Units, and the\r\nlinker tends to put library units at the start of the code section, and user units at the end. So everything at the end of the\r\nCODE section is likely to be user code and thus interesting. Let us have a look at the function using HexRays:\r\n 1\r\n 2\r\n 3\r\n 4\r\n 5\r\n 6\r\n 7\r\n 8\r\n 9\r\n10\r\n11\r\n12\r\n13\r\n14\r\n15\r\n16\r\n#! cpp\r\nint DropAndRun() { // 46D330\r\n int *v0; // eax\r\n DWORD v3; // [esp-18h] [ebp-20h]\r\n int *v4; // [esp-14h] [ebp-1Ch]\r\n LPURL_COMPONENTS v5; // [esp-10h] [ebp-18h]\r\n unsigned int v6; // [esp-Ch] [ebp-14h]\r\n void *v7; // [esp-4h] [ebp-Ch]\r\n __int32 unpacked_bitmap; // [esp+0h] [ebp-8h] BYREF\r\n int v9; // [esp+4h] [ebp-4h] BYREF\r\n int savedregs; // [esp+8h] [ebp+0h] BYREF\r\n TimeGetTickCount(100);\r\n sub_406F00(dword_48ACB0, dword_48AD7C, 4);\r\n if ( InetIsOffline(0) )\r\n System::__linkproc__ LStrAsg(\u0026payload, \u0026str_A[1]);\r\nhttps://malcat.fr/blog/exploit-steganography-and-delphi-unpacking-dbatloader/\r\nPage 5 of 16\n\n17\r\n18\r\n19\r\n20\r\n21\r\n22\r\n23\r\n24\r\n25\r\n26\r\n27\r\n28\r\n29\r\n30\r\n31\r\n32\r\n33\r\n34\r\n35\r\n36\r\n37\r\n38\r\n else\r\n System::__linkproc__ LStrAsg(\u0026payload, \u0026str_a[1]);\r\n System::__linkproc__ LStrCatN(\u0026v9, 6);\r\n GetApiAddress(v9, \u0026str_RasClearLinkSta[1], \u0026p_RasClearLinkStatistics);\r\n if ( dword_48ACC0 \u003c= 0x87A68E ) { // always true\r\n GetApiAddress(\u0026str_amsi[1], \u0026str_amsiamsiScanBuf[1], \u0026p_amsiScanBuffer);\r\n Patch(p_amsiScanBuffer, WinHttpCrackUrl);\r\n bitmap = Graphics::TBitmap::TBitmap(\u0026cls_Graphics_TBitmap);\r\n LoadResourceIntoBitmap(bitmap, Y, \u0026str_BBTREX + 8);// load resource bitmap BBTREX\r\n SteganoUnpack(bitmap, \u0026unpacked_bitmap); // extract payload from bitmap\r\n System::__linkproc__ LStrAsg(\u0026payload, unpacked_bitmap);\r\n RunPayload(payload); // run payload in memory\r\n v0 = j_unknown_libname_57_0(\u0026dword_48AD44);\r\n System::Move(aGlojdxoscdjtlq, v0 + 2, 0); // append very long string in memory\r\n }\r\n else {\r\n // call WinHttpCrackUrl and exits\r\n }\r\n __writefsdword(0, v6);\r\n v7 = \u0026loc_46D4A5;\r\n return System::__linkproc__ LStrArrayClr(\u0026unpacked_bitmap, 2);\r\n}\r\nThe function RunPayload at address 0x46cdf0 makes use of VirtualAlloc and VirtualProtect , which suggests that\r\nat this point the dropper already decrypted its payload. And just before the call, we can see that the program loads the\r\npatched bitmap resource BBTREX into a TBitmap and calls the function that we named SteganoUnpack . So let us have a\r\nlook at SteganoUnpack .\r\nDecrypting the bitmap\r\nThe function SteganoUnpack at address 0x46C8F8 is a bit harder to understand. But using IDA's Delphi FLIRT signatures,\r\nwe can get most of it:\r\n 1\r\n 2\r\n 3\r\n 4\r\n 5\r\n 6\r\n 7\r\n 8\r\n 9\r\n10\r\n11\r\n12\r\n13\r\n14\r\n15\r\n16\r\n17\r\n18\r\n19\r\n20\r\n21\r\n22\r\n23\r\n24\r\n25\r\n26\r\nint __fastcall SteganoUnpack(Graphics::TBitmap *bitmap, BYTE *output)\r\n{\r\n char is_bit_set; // al\r\n payload_data = new_string(\u0026off_415BF8, output, 0);\r\n line_content = Graphics::TBitmap::GetScanline(bitmap, 0);// read first bitmap line\r\n // in the first 3 bytes is an integer encoded that is the number of lsb bits that should be extracted from each byte to get th\r\n // (only saw the value 3 in the wild)\r\n stegano_num_lsb_bits = 0;\r\n is_bit_set = IsBitSet(*line_content, 0);\r\n SetBit(\u0026stegano_num_lsb_bits, 0, is_bit_set);\r\n is_bit_set = IsBitSet(*line_content, 1u);\r\n SetBit(\u0026stegano_num_lsb_bits, 1, is_bit_set);\r\n is_bit_set = IsBitSet(*(line_content + 1), 0);\r\n SetBit(\u0026stegano_num_lsb_bits, 2, is_bit_set);\r\n is_bit_set = IsBitSet(*(line_content + 2), 0);\r\n SetBit(\u0026stegano_num_lsb_bits, 3, is_bit_set);\r\n bitmap_width = (*(*bitmap + 44))(bitmap);\r\n bitmap_height = (*(*bitmap + 32))(bitmap) - 1;\r\n bitmap_line_index = 0;\r\n bit_index = 0;\r\n bitmap_row_index = 2; // X position on current bitmap line (1-based)\r\n rgb_index = 1; // which color component are we reading: 1 = RED, 2 = GREEN, B = BLUE\r\n line_content += 3; // advance file pointer by 3 bytes\r\nhttps://malcat.fr/blog/exploit-steganography-and-delphi-unpacking-dbatloader/\r\nPage 6 of 16\n\n27\r\n28\r\n29\r\n30\r\n31\r\n32\r\n33\r\n34\r\n35\r\n36\r\n37\r\n38\r\n39\r\n40\r\n41\r\n42\r\n43\r\n44\r\n45\r\n46\r\n47\r\n48\r\n49\r\n50\r\n51\r\n52\r\n53\r\n payload_bit_index = 0;\r\n do {\r\n is_bit_set = IsBitSet(*(line_content + rgb_index - 1), bit_index);\r\n SetBit(\u0026payload_size, payload_bit_index, is_bit_set);\r\n AdvanceToNextBit(); // will update bit_index, rgb_index, bitmap_row_index and line_content as needed\r\n ++payload_bit_index;\r\n } while ( payload_bit_index != 32 ); // first 32 payload bits = payload size\r\n if ( payload_size \u003e 0 ) { // start of the payload extraction process\r\n do {\r\n payload_bit_index = 0;\r\n do {\r\n is_bit_set = IsBitSet(*(line_content + rgb_index - 1), bit_index);\r\n SetBit(\u0026payload_byte, payload_bit_index, is_bit_set);\r\n AdvanceToNextBit(); // will update bit_index, rgb_index, bitmap_row_index and line_content as needed\r\n ++payload_bit_index;\r\n }\r\n while ( payload_bit_index != 8 ); // extract 8 bits from bitmap\r\n (*(*payload_data + 16))(payload_data, \u0026payload_byte, 1);// append byte to payload data\r\n System::__linkproc__ LStrAsg(output, *(payload_data + 1));\r\n }\r\n while ( --payload_size ); // read \u003cpayload_size\u003e bytes into \u003coutput\u003e\r\n }\r\n System::TObject::Free(payload_data);\r\n return System::TObject::Free(bitmap);\r\n}\r\nIn a nutshell, the function reads the bitmap line by line, and each line pixel by pixel. For every byte of the bitmap, some bits\r\n(the lowest significant bits) are extracted and concatenated in order to assemble the final payload. This is textbook\r\nsteganography. The first line is a bit special since it contains additional info:\r\nThe first 3 bytes (so the first RGB pixel) encodes a 4 bits integer (2 bits of red component, 1 bit of green and 1 bit of\r\nblue). This integer that we named stegano_num_lsb_bits tells the software how many bits of each bitmap byte it\r\nshould extract from the image (3 in our case)\r\nThen the software jumps to the 4th byte and reads 32 bits from the bitmap into an integer. This integer is the number\r\nof bytes which should be extracted from the image (the payload size in other words)\r\nFinally the software starts the payload extraction process\r\nSo let us try if we got it right. We will open the bitmap BBTREX (which is a DIB bitmap, meaning the\r\nBITMAPFILEINFOHEADER is missing) in an hexadecimal editor and try to manually decode the first bytes. We first have\r\nto locate the first bitmap row. Good to know: bitmaps are stored upside down, i.e the top-most line is actually the last one in\r\nthe file. So knowing that our bitmap is 588 pixels wide and is a RGB bitmap (so 3 bytes per pixel), the first line should start\r\nat EndOfFile - 588*3 = 0x44ea8 :\r\nFigure 7: The first bitmap line\r\nSo first thing first, we will decrypt the first 4 bits integer (aka stegano_num_lsb_bits ). The first line starts with the 3 bytes\r\n03 02 02 , which gives us the binary number 1100 (in LSB display) = 3. Ok.\r\nhttps://malcat.fr/blog/exploit-steganography-and-delphi-unpacking-dbatloader/\r\nPage 7 of 16\n\nNext, the algorithm moves to the second pixel and reads 32 bits. 32 bits / 3 bits per byte means it will read 10 bytes and 2\r\nbits of the 11th byte. The next 11 bytes are: 00 00 00 03 06 02 00 00 00 00 04 , which gives us the binary number 000\r\n000 000 110 011 010 000 000 000 000 00(1) (in LSB display) = 91648 ok. The 11th byte contains an additional bit which\r\nwe did not read which was a (1) .\r\nNext we could start reading 2 bytes of the payload, which is 16 bits. Since we still have a bit unread from 04 , we just have\r\nto read 15 additional bits or 5 bytes. The next five bytes are: 06 04 04 06 02 , which gives us the binary numbers (1) 011\r\n001 0--01 011 010 or 0x4d--0x5a ... looks like the start of a PE, great!\r\nSo let us put everything together and write a small extraction script using python. The following script should be run inside\r\nMalcat's script editor with the bitmap open:\r\n 1\r\n 2\r\n 3\r\n 4\r\n 5\r\n 6\r\n 7\r\n 8\r\n 9\r\n10\r\n11\r\n12\r\n13\r\n14\r\n15\r\n16\r\n17\r\n18\r\n19\r\n20\r\n21\r\n22\r\n23\r\n24\r\n25\r\n26\r\n27\r\n28\r\n29\r\n30\r\n31\r\n32\r\n33\r\n34\r\n35\r\n36\r\n37\r\n38\r\n39\r\n40\r\n41\r\n42\r\n43\r\n44\r\n45\r\n46\r\n47\r\ndef next_byte(malcat):\r\n width = malcat.struct[\"BitmapInfoHeader\"][\"biWidth\"]\r\n height = malcat.struct[\"BitmapInfoHeader\"][\"biHeight\"]\r\n bpp = malcat.struct[\"BitmapInfoHeader\"][\"biBitCount\"]\r\n data = malcat.struct[\"biImageData\"]\r\n line_width = width * (bpp // 8)\r\n if line_width % 4:\r\n line_width += 4 - (line_width % 4)\r\n for y in range(height, 0, -1):\r\n ptr = line_width * (y - 1)\r\n for x in range(width * (bpp // 8)):\r\n yield data[ptr + x]\r\ndef next_bit(data):\r\n for c in data:\r\n for i in range(num_bits):\r\n yield (c \u003e\u003e i) \u0026 1\r\nbyte_iterator = next_byte(malcat)\r\nfor i in range(3):\r\n next(byte_iterator)\r\nbit_iterator = next_bit(byte_iterator)\r\nres = bytearray()\r\n# read size of payload\r\npayload_size = 0\r\nfor i in range(32):\r\n payload_size |= next(bit_iterator) \u003c\u003c i\r\ncur_i = 0\r\ncur_val = 0\r\nfor bit in bit_iterator:\r\n if bit:\r\n cur_val = cur_val | (1 \u003c\u003c cur_i)\r\n cur_i += 1\r\n if cur_i \u003e= 8:\r\n res.append(cur_val)\r\n if len(res) == payload_size:\r\n break\r\n cur_val = 0\r\n cur_i = 0\r\ngui.open_after(bytes(res), \"decrypted\")\r\nhttps://malcat.fr/blog/exploit-steganography-and-delphi-unpacking-dbatloader/\r\nPage 8 of 16\n\nRunning the script gives us the second stage of DBatLoader.\r\nSecond stage: cloud download\r\nWe are now looking at a Delphi binary of sha256 e232e1cd61ca125fbb698cb32222a097216c83f16fe96e8ea7a8b03b00fe3e40\r\n(VT). Given its small size (91KB) and API usage (wininet usage) it definitely looks like a downloader. So let us dive in this\r\nnew binary.\r\nRetrieving the url\r\nWho says downloader says download url, but no URL can be found in the second stage. If the url is not hard-coded in this\r\nbinary, it has to be somewhere else. Remember the big big string that we've identified as suspicious in the previous binary\r\n(address 0x0046f718 )? It is mostly composed of uppercase letter, except for a short substring:\r\nFigure 8: Huge string composed almost exclusively of capital letters\r\nAnd the delimiter ^^Nc can be found as referenced string in the second stage binary at address 0x413f58 , so could it be\r\nour url? At this point we should look for decrypting functions inside one of the two binaries. But let us be smart. See how\r\nthe string prefix ammil3(( has repeated characters. Encryption is must likely a weak one-byte cipher. And we know that we\r\nare looking for an url, so the plain text string could definitely start with https:// . So let us try a few usual cipher:\r\nXOR: the key would be 0x09 and give us hdd... -\u003e no\r\nROT13: ROT13 does not encode non-letter characters so not likely since the slash has been encrypted\r\nADD: the key would be 0x7 and give us\r\nhttps://cdn.discordapp.com/attachments/902132472924479511/902136733435592744/Wbjhzkbevojgqfhfalbqxnykvunmobi\r\n... bingo!\r\nSometimes, being lazy pays off. Note that the url is not reachable anymore at the time of writing, so I have attached a copy\r\nof the file at this address. But the work is not over yet: the downloaded packet looks encrypted:\r\nFigure 9: Repeating sequence in downloaded buffer\r\nDecrypting the file\r\nSo before going further, we have to locate function responsible for decrypting the downloaded discord attachment inside the\r\nbinary. While the binary is relatively small, Malcat helps us saving some time by locating two candidates functions featuring\r\na XOR opcode inside a loop:\r\nhttps://malcat.fr/blog/exploit-steganography-and-delphi-unpacking-dbatloader/\r\nPage 9 of 16\n\nFigure 10: any XOR in a loop is a good decryption routine candidate\r\nThe function sub_413b14 seems to be the most promising of the two, so let us have a look. This function is quite simple,\r\nand takes as input a single number in ecx and a Delphi string in edx . The number is kind of the decryption key, and will\r\nbe used to generate three variables:\r\n[ebp-0C] which is initialized with 0x833e - number\r\n[ebp-10] which is initialized with 0x5e9b - number\r\n[ebp-14] which is initialized with 0x41d6 - number\r\nThis input number is hard-coded. If we look to the decryption function's caller code, we can see that this numbers stems\r\nfrom an atoi(0x41414c) call at address 0x41408d . The atoi parameter at address 0x41414c is the string \"328\" , so\r\nthe first mystery has been solved.\r\nFigure 11: decryption function sub_413b14\r\nNow we just have to figure how the key stream is generated from these three variables. The assembly code of the function\r\nbody is relatively simple. We converted it to a Python script that can be run inside Malcat, with the downloaded file open.\r\nRunning the script will decrypt the packet:\r\n 1\r\n 2\r\ndef decrypt_stage2(data, number):\r\n res = bytearray(len(data))\r\nhttps://malcat.fr/blog/exploit-steganography-and-delphi-unpacking-dbatloader/\r\nPage 10 of 16\n\n3\r\n 4\r\n 5\r\n 6\r\n 7\r\n 8\r\n 9\r\n10\r\n11\r\n12\r\n13\r\n14\r\n15\r\n16\r\n17\r\n18\r\n19\r\n20\r\n ebp_c = 0x833e - number\r\n ebp_10 = 0x5e9b - number\r\n ebp_14 = 0x41d6 - number\r\n di = ebp_14\r\n for i, c in enumerate(data):\r\n e = c ^ (di \u003e\u003e 8)\r\n res[i] = e\r\n di = c\r\n di = (di + ebp_14) \u0026 0xffff\r\n di = (di * ebp_c) \u0026 0xffff\r\n di = (di + ebp_10) \u0026 0xffff\r\n return res\r\ndecrypted = decrypt_stage2(malcat.file[:], 328)\r\ndecrypted = decrypted[::-1] # payload is stored reversed, don't ask me why\r\ngui.open_after(bytes(decrypted), \"decrypted\")\r\nAfter decryption, we obtain yet another Delphi program, which would make it the third stage of the malware.\r\nThird stage: resource dropper\r\nWe are now looking at a Delphi binary of sha256 f8fc925d89baa140c9cb436f158ec91209789e9f8e82a0b7252f05587ce8e06f\r\n(VT). It looks more like a dropper this time, since most of its size (269KB) is taken by a single resource entry named YAK .\r\nFigure 12: Third stage of the malware: a dropper\r\nThe YAK resource is a well-known artifact of the DBatLoader malware family. Note that Malcat does not identify it as a\r\nDelphi program because section names have been modified post-compilation and replaced with dots. Why, that's a very good\r\nquestion, since it only makes the binary more suspicious.\r\nMaking sense of the YAK resource\r\nThe program contains all his logic inside the main function, located at the program's entry point. It performs a lot of\r\nunnecessary and over-complicated operations in order to decrypt the resource. Here is a summary:\r\ncall to function 0x416004 : loads content of resource YAK into memory\r\ncall to function 0x416408 : the resource bytes gets \"decrypted\" using the following algorithm: for every byte b, if\r\n0x21 \u003c= b \u003c= 0x7e: b = ( (c + 0xe) % 0x5e) + 0x21. I know, it does not make a lot of sense.\r\nthe first 36 bytes of the decrypted resource is a delimiter ( *()%@5YT!@#G__T@#$%^\u0026*()__#@$#57$#!@ ). This delimiter\r\nis used to separate different fields in the decrypted YAK data:\r\nthe first field ( 7826546 ) use is unknown\r\nhttps://malcat.fr/blog/exploit-steganography-and-delphi-unpacking-dbatloader/\r\nPage 11 of 16\n\nthe second field is a XOR key used to decrypt the payload data\r\nthe third field is used to generate the filename and RunKey name used by the dropper to save and persist the\r\ndropped payload data\r\nthe 4th field is the encrypted payload data\r\n... other field of lesser importance follow\r\nthe last field is another decryption key and has the value 328 (remember stage 1? Looks like the author\r\nreally likes this number)\r\nThis is how the YAK resource looks after the first initial decryption done by function 0x416408 . We have highlighted the\r\ndelimiter to better highlight the different fields:\r\nFigure 13: The decrypted YAK resource first 4 fields\r\nDoes it sound overly complicated? Wait until you have seen how the resource payload data is decrypted.\r\nDecrypting the payload data\r\nNow that we know the structure of the YAK resource, it is time to decrypt the payload data (aka the 4th field), which makes\r\nmost of the YAK resource. The decryption process happens in four steps:\r\nfunction 0x415c40 decrypts Xor the data using the second field ( ipnwxoenebxarqdhdiseentqdtfigqgzpuxlxi ) as\r\nkey. But every byte is not only XORed with a byte of the key, but also with the size of the payload AND the size of\r\nthe key.\r\nthe result is reversed.\r\nfunction 0x416368 decrypts the final result using the last field ( 328 ) as key. Every byte is added with the value\r\n335 % 328 = 7 .\r\nthe result is finally decrypted using function 0x416408 , the same algorithm that was used to perform the initial\r\ndecryption of the YAK resource\r\nAt this stage, I have a lot of questions to the programmer who wrote this. The main one is: why oh god why? Why adding so\r\nmuch complexity to the payload extraction process. The added measures don't help evading detection:\r\nmanual reversers don't care about the extra layers. Most of them youl just use a debugger and go through the\r\ndecryption process in one pass.\r\nfor reversers who likes to do everything statically (hi!), the added code is too simple to be considered as obfuscation.\r\nantivirus programs don't care about the resource, they would just put a signature on the decryption code. Or even\r\nbetter, create an heuristic on the binary (which would be very easy considering Delphi program with dots as section\r\nnames are pretty rare :)\r\n\"next-gen\" machine-learning based antivirus also have a very easy time there\r\nsandbox directly go through the decryption process and would grab the payload at injection time\r\nOn the other hand, it makes the dropping code quite harder to maintain. I am a bit puzzled to be honest. Anyway, let us write\r\nthe decryption algorithm in python. This python scripts must be run inside Malcat, with the third stage binary open:\r\n 1\r\n 2\r\n 3\r\n 4\r\n 5\r\nimport itertools\r\ndef decrypt_yak(data):\r\n \"\"\"\r\n implements the first decryption layer of function 0x416408\r\nhttps://malcat.fr/blog/exploit-steganography-and-delphi-unpacking-dbatloader/\r\nPage 12 of 16\n\n6\r\n 7\r\n 8\r\n 9\r\n10\r\n11\r\n12\r\n13\r\n14\r\n15\r\n16\r\n17\r\n18\r\n19\r\n20\r\n21\r\n22\r\n23\r\n24\r\n25\r\n26\r\n27\r\n28\r\n29\r\n30\r\n31\r\n32\r\n33\r\n34\r\n35\r\n36\r\n37\r\n38\r\n39\r\n40\r\n41\r\n42\r\n43\r\n44\r\n45\r\n46\r\n47\r\n48\r\n49\r\n50\r\n51\r\n52\r\n53\r\n54\r\n55\r\n56\r\n57\r\n58\r\n59\r\n60\r\n61\r\n62\r\n63\r\n \"\"\"\r\n res = bytearray(data)\r\n for i, c in enumerate(data):\r\n if 0x21 \u003c= c \u003c= 0x7e:\r\n res[i] = ((((c + 0xe) % 0x5e) + 0x21) \u0026 0xff)\r\n return res\r\ndef xor_payload(data, key):\r\n \"\"\"\r\n custom XOR, function 0x415c40\r\n \"\"\"\r\n res = bytearray()\r\n for data_byte, key_byte in zip(data, itertools.cycle(key)):\r\n c = data_byte ^ key_byte ^ len(data) ^ len(key)\r\n res.append( c \u0026 0xff )\r\n return res\r\ndef add_payload(data, key):\r\n \"\"\"\r\n custom ADD, function 0x416368\r\n \"\"\"\r\n res = bytearray(len(data))\r\n val = 335 % key\r\n for i, c in enumerate(data):\r\n res[i] = (c + val) \u0026 0xff\r\n return res\r\n#######################################\r\nyak_resource = malcat.struct[\"Resources.RCDATA.YAK.unk.Data\"]\r\ndecrypted = decrypt_yak(yak_resource)\r\n# split resource into fields\r\ndelimiter = decrypted[:36]\r\nfields = decrypted[len(delimiter):].split(delimiter)\r\n#get important fields\r\npayload_data = fields[3]\r\nxor_key = fields[1]\r\nadd_key = int(fields[-1])\r\nprint(\"Decrypting payload data ({} bytes) with XOR key '{}' and ADD key {:d}\".format(\r\n len(payload_data),\r\n xor_key.decode(\"ascii\"),\r\n add_key))\r\nstep1 = xor_payload(payload_data, xor_key)\r\nstep2 = step1[::-1] # reverse\r\nstep3 = add_payload(step2, add_key)\r\ndecrypted = decrypt_yak(step3)\r\ngui.open_after(bytes(decrypted), \"yak_payload_plaintext\")\r\nRunning this script, we obtain another PE file. Do you think it is the final malware? Do you? Of course not :)\r\nhttps://malcat.fr/blog/exploit-steganography-and-delphi-unpacking-dbatloader/\r\nPage 13 of 16\n\nFourth stage: Stone's packer\r\nThis time, believe it or not, we are not facing a Delphi program, but a packed 164KB binary featuring a weird .Stone\r\nsection and a huge encrypted .text section. The sha256 of the binary is\r\nb0b4a3897ef76dfebc9ccdc9b83b49cb6d23c08a5b010bf8960c0bb82d48c4bc . How do we know it is packed you may ask?\r\nWell, it could be because the entropy is high, or maybe because it is written in the binary:\r\nFigure 14: That is one hell of a stealth crypter\r\nYes, sometimes it is this easy :) Also the word PowerLame seems to imply we won't have a hard time cracking this one.\r\nUnpacking Stone's packer\r\nInstead of diving into the code, let us have a quick sweep through the file. The .text section displays interesting\r\nproperties, in particular the beginning of the section:\r\nFigure 15: Start of encrypted .text section\r\nThe end of the first section is also interesting. Sections are usually padded with zeroes (or PADDINGXXX for the resource\r\nsection), but here we got ones instead:\r\nFigure 16: End of encrypted .text section\r\nKnowing that the most frequent x86 function prologue is 55 8B EC (aka push ebp; mov ebp, esp ), it looks like all bytes\r\nvalue are just one off. So let us try our hypothesis and just subtract 1 to the complete .text section. This can be done\r\neasily using Malcat's transforms, as we can see below:\r\nFigure 17: Decrypting the .text section\r\nhttps://malcat.fr/blog/exploit-steganography-and-delphi-unpacking-dbatloader/\r\nPage 14 of 16\n\nAfter reanalyzing the file, we can see that our hypothesis holds and the .text section has been successfully decrypted.\r\nSeveral functions are now visible, even if most of theme are obfuscated and part of the binary seem to remain encrypted. But\r\nanyway, we are now facing the last stage of the malware, and what we see should be enough to identify the malware.\r\nIdentifying the malware family\r\nUsing the TLP:white Yara rule set from Malpedia, the decrypted binary is detected by Malpedia's Formbook rule:\r\nFigure 18: Formbook detection\r\nFormbook is a well-known stealer-as-a-service used by a variety of threat actors for over five years. It is designed to steal\r\npersonal information and allow remote control via commands issued from a C2 server. It can steal passwords from locally\r\ninstalled software (browsers, chat clients, email clients and FTP clients), or directly from the user using keylogger and form-grabber components. After submitting the sample toe Joe sandbox, we get access to the Formbook configuration data and the\r\naddress of its C2 server:\r\n 1\r\n 2\r\n 3\r\n 4\r\n 5\r\n 6\r\n 7\r\n 8\r\n 9\r\n10\r\n11\r\n12\r\n13\r\n14\r\n15\r\n16\r\n{\r\n \"C2 list\": [\r\n \"www.mgav26.xyz/n8rn/\"\r\n ],\r\n \"decoy\": [\r\n \"jlvip1066.com\",\r\n \"gconsultingfirm.com\",\r\n \"foundergomwef.xyz\",\r\n \"bredaslo.com\",\r\n // ... (truncated)\r\n \"counterpokemon.com\",\r\n \"beyerenterprisestreeservice.com\",\r\n \"phorganicfoods.com\",\r\n \"hermespros.com\"\r\n ]\r\n}\r\nAnd ... that's the end of the infection chain and the end of this article.\r\nConclusion\r\nWhile entry-level malware do not make headlines, it does not mean that they should be ignored altogether. Some of them are\r\nmore than just mere droppers and feature multi-staged architectures. In this article, we have dissected a gran total of 4\r\nintermediate malicious binaries that were used between the initial infection (an armed Excel spreadsheet) and the final\r\nmalware (Formbook).\r\nEach of them used different techniques, from exploits to cloud-based downloaders and event a bit of steganography. We\r\ndeveloped python scripts to extract and decrypt the payload of each of them. These scripts can be applied to other instances\r\nof DBatLoader, like this other excel document, which downloads another DBatLoader first stage using yet another picture\r\nfor its steganography.\r\nhttps://malcat.fr/blog/exploit-steganography-and-delphi-unpacking-dbatloader/\r\nPage 15 of 16\n\nSource: https://malcat.fr/blog/exploit-steganography-and-delphi-unpacking-dbatloader/\r\nhttps://malcat.fr/blog/exploit-steganography-and-delphi-unpacking-dbatloader/\r\nPage 16 of 16",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://malcat.fr/blog/exploit-steganography-and-delphi-unpacking-dbatloader/"
	],
	"report_names": [
		"exploit-steganography-and-delphi-unpacking-dbatloader"
	],
	"threat_actors": [
		{
			"id": "b740943a-da51-4133-855b-df29822531ea",
			"created_at": "2022-10-25T15:50:23.604126Z",
			"updated_at": "2026-04-10T02:00:05.259593Z",
			"deleted_at": null,
			"main_name": "Equation",
			"aliases": [
				"Equation"
			],
			"source_name": "MITRE:Equation",
			"tools": null,
			"source_id": "MITRE",
			"reports": null
		},
		{
			"id": "aa73cd6a-868c-4ae4-a5b2-7cb2c5ad1e9d",
			"created_at": "2022-10-25T16:07:24.139848Z",
			"updated_at": "2026-04-10T02:00:04.878798Z",
			"deleted_at": null,
			"main_name": "Safe",
			"aliases": [],
			"source_name": "ETDA:Safe",
			"tools": [
				"DebugView",
				"LZ77",
				"OpenDoc",
				"SafeDisk",
				"TypeConfig",
				"UPXShell",
				"UsbDoc",
				"UsbExe"
			],
			"source_id": "ETDA",
			"reports": null
		}
	],
	"ts_created_at": 1775433979,
	"ts_updated_at": 1775826734,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/604d26a9283e1d6a33464610b9b2b4c25e4989ce.pdf",
		"text": "https://archive.orkl.eu/604d26a9283e1d6a33464610b9b2b4c25e4989ce.txt",
		"img": "https://archive.orkl.eu/604d26a9283e1d6a33464610b9b2b4c25e4989ce.jpg"
	}
}