{
	"id": "361cab11-da96-476e-a21c-dbc12a5e5e15",
	"created_at": "2026-04-06T00:09:28.226037Z",
	"updated_at": "2026-04-10T03:21:10.083166Z",
	"deleted_at": null,
	"sha1_hash": "8ac5a064eaf8c4cd0215f571f14011a9aeb6cba4",
	"title": "Writing a Qakbot 5.0 config extractor with Malcat",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 4124783,
	"plain_text": "Writing a Qakbot 5.0 config extractor with Malcat\r\nBy Malcat EI\r\nArchived: 2026-04-05 13:22:27 UTC\r\nSample:\r\n73472cfc52f2732b933e385ef80b4541191c45c995ce5c42844484c33c9867a3.msi (Bazaar, VT)\r\nInfection chain:\r\nMSI installer -\u003e Backdoored DLL -\u003e PE loader -\u003e Qakbot\r\nTools used:\r\nMalcat\r\nDifficulty:\r\nIntermediate\r\nIntroduction\r\nQakbot has been studied a lot over the last 15 years, and plays a big role in the malware landscape. After a\r\nsuccessful takedown that took place in August 2023, it got a bit of attention lately as a new variant has been\r\nspotted around December 2023.\r\nBut after raising from the dead, the RAT also switched to a new version: 5.0. Sadly the existing Qakbot\r\nconfiguration extractors stopped working (as far as I know), suggesting that the malware code underwent non-trivial changes. That is relatively annoying: configuration extractors are really useful for botnet tracking and\r\nincident response. But instead of complaining, let us fire up Malcat and see if we can write a configuration\r\nextractor ourselves!\r\nFirst stage: MSI installer\r\nThanks to Malware Bazaar, it was rather easy to find a recent Qakbot sample. This one happens to be a MSI\r\ninstaller. MSI installers are often abused by malware authors to package their malicious programs. So let us load\r\nthe file in Malcat and see what we got. A first look in the summary view tells us that we are facing an \"Acrobat\"\r\ninstaller. Sure.\r\nhttps://malcat.fr/blog/writing-a-qakbot-50-config-extractor-with-malcat/\r\nPage 1 of 24\n\nFigure 1: The installer\r\nThe first thing to look at when analysing a MSI installer is the CustomAction table, which somewhat drives the\r\ninstallation process. Luckily, Malcat full \u0026 pro can display the content of all MSI tables in the decompiler view.\r\nJust press F4 and scroll down to the CustomAction table (tables are sorted alphabetically). Entries of type\r\nLaunchFile are particularly interesting, and there is indeed one running a program named viewer.exe with a\r\npretty suspicious command line:\r\n{\r\n \"Action\": \"LaunchFile\",\r\n \"Type\": 2,\r\n \"Source\": \"viewer.exe\",\r\n \"Target\": \"/HideWindow rundll32 [APPDIR]\\\\MicrosoftOffice15\\\\ClientX64\\\\[ProductName].dll,CfGetPlatformInfo\"\r\n \"ExtendedType\": null\r\n}\r\nWe will first have a look at this viewer.exe . In my experience, there are two types of files in a MSI installer:\r\nFiles that are just needed during the installation: pictures, plugins, tools etc. These files are stored inside\r\nthe Binary database. Malcat will list them as Binary.\u003cfilename\u003e in the Virtual File System tab.\r\nFiles permanently installed to disk. These are stored inside a CAB archive, like the disk1.cab file in this\r\ninstaller.\r\nOur file viewer.exe seems to be of the first type, and we just have to double-click Binary.viewer.exe in the\r\nVirtual File System tab to open it. A quick threat intelligence hash lookup (Ctrl+I or Check intelligence button\r\nfrom the summary view) suggests us that the file might be a simple third-party launcher:\r\nhttps://malcat.fr/blog/writing-a-qakbot-50-config-extractor-with-malcat/\r\nPage 2 of 24\n\nFigure 2: The file viewer.exe\r\nNext in line of suspects is the DLL referenced in the Target property. We don't have the name of the DLL, but\r\nluckily for us there is a single DLL file named dll_1 in the file disk1.cab . To open it, just double-click on\r\ndisk1.cab and then on dll_1 . We are now facing the second stage of the infection.\r\nSecond stage: Antimalw.dll\r\nThe file dll_1 is a 922KB PE DLL of sha256\r\na59707803f3d94ed9cb429929c832e9b74ce56071a1c2086949b389539788d8a (Virusshare, VT) named either\r\nantimalw.dll (version infos) or antimalware_provider64.dll (export name). The file immediately strikes us\r\nas suspicious:\r\nIt claims to be Bitdefender's AMSI provider, that is the script scannning component of the Bitdefender\r\nantivirus. antimalw.dll contains parts of Bitdefender's original DLL, but clearly isn't.\r\nIts data directory suggests that it is signed with a certificate, but the location of the certificate has been\r\noverwritten by the .rsrc section\r\nIt has one large high-entropy resource named ЬГнЦИРИ\r\nIts entry-point function is empty\r\nIt has a single exported function CfGetPlatformInfo which seems obfuscated\r\nIt looks that the malware author took Bitdefender's antimalware_provider64.dll and backdoored/overwrote it\r\nwith malicious code.\r\nhttps://malcat.fr/blog/writing-a-qakbot-50-config-extractor-with-malcat/\r\nPage 3 of 24\n\nFigure 3: A suspicious DLL\r\nNow that we have verified that the file is malicious, back to business. The first step I take when facing a packed\r\nmalware is a process I call Where is the poop, Robin. See, there is no magic: malware have to store their payload\r\nsomewhere (unless they're downloaders of course). So instead of diving blindly into the code or submitting the\r\nbinary to a slow sandbox, it is often best to first locate the encrypted payload. Finding the hidden payload either\r\nallows you to decrypt it immediately or, worst case scenario, will give you useful pointers to start your reverse\r\nengineering.\r\nThe large high-entropy resource ЬГнЦИРИ seems like a good candidate to start our search. Scrolling through its\r\nbytes in the hexadecimal view, we can see a repeating pattern near the end of the file. This usually suggests some\r\nkind of rotating key encryption mechanism. Since there is a huge chance that the end of the file are zeroes, and\r\nsince we know that malware authors love their XOR encryption, we will simply try to un-xor it with the key\r\n\"HU03!Mm!?qYHCTnaEX\u003c\\0\" (note the ending null byte). Incidentally, this string appears as a stack string in the\r\nexported function CfGetPlatformInfo , which is encouraging:\r\nhttps://malcat.fr/blog/writing-a-qakbot-50-config-extractor-with-malcat/\r\nPage 4 of 24\n\nFigure 4: Decrypting the resource\r\nAnd indeed, we have successfully decrypted the resource. Long live XOR encryption!\r\nStages 3: PE loader\r\nWe are now facing what looks like a 180KB x64 shellcode (sha256\r\n8c7401218e6da9533d4e97849ad6c528b231c1b9cdcf43d1788757c3862dc2d4 ). Now there are two ways to go forth.\r\nThe obvious one is to emulate the shellcode, which can be done following the steps below:\r\n1. Force the architecture to x64\r\n2. Select first byte of the shellcode and define a new function there\r\n3. Try your luck with one of Malcat's emulator script, for instance running the script emulation/Speakeasy\r\n(shellcode)\r\nOn the other hand, Malcat did carve a 170KB plain text PE file out of the 180 KB shellcode. So let us take the\r\neasy way and just grab the next stage by double-clicking the carved PE file:\r\nhttps://malcat.fr/blog/writing-a-qakbot-50-config-extractor-with-malcat/\r\nPage 5 of 24\n\nFigure 5: The shellcode and it embedded PE file\r\nStage 4: the Qakbot DLL\r\nThe next stage is a 170KB PE dll of sha256\r\naf6a9b7e7aefeb903c76417ed2b8399b73657440ad5f8b48a25cfe5e97ff868f (Virusshare, VT) named cldapi.dll .\r\nWe are facing the final stage of the infection chain: a Qakbot malware compiled the 2024-01-29, so most likely\r\none of the new 5.0 version!\r\nHow can we be sure it's the final malware? Usually I tend to confirm with Malpedia's Yara rules, but sadly their\r\nYara rule don't seem to cover the new Qakbot version. But if we compare our cldapi.dll sample against a\r\nQakbot version from March 2023 (e.g. this one), we can see that even if some strings were changed or got\r\nencrypted, most are still there:\r\nFigure 6: Strings comparison against a Qakbot sample from march 2023\r\nhttps://malcat.fr/blog/writing-a-qakbot-50-config-extractor-with-malcat/\r\nPage 6 of 24\n\nBeside the Qakbot attribution, we can see that the DLL is slightly obfuscated:\r\nAPI addresses are resolved dynamically by hash at runtime (hashes are encrypted)\r\nMost strings are encrypted\r\nThere are a few junk code islands here and there\r\nWhile API obfuscation is not a big deal in our case, the string encryption might be problematic if we want to write\r\na configuration extractor. This will be our first task: locate and decrypt Qakbot's strings.\r\nDecrypting strings\r\nLocating the first encrypted string array\r\nWhile Qakbot is not a huge malware, reversing more than 120KB of code will always be tedious. And since we\r\nare looking for something rather precise, an encrypted data blob, we will again focus on the data instead instead of\r\ndiving into the code. More precisely, we will try to find all data buffers in the any data section which are:\r\nrelatively large, let's say more than 64 bytes\r\nhave a high entropy\r\nhave incoming code references\r\nTo ease your search, make sure that you have enabled the incoming references highlighting.\r\nBy chance Malcat already identifies a few known constant arrays such as precomputed tables used by the\r\nembedded Zlib library, which saves us some time as these buffers are not interesting to us. Starting from address\r\n0x180028150 , we can see a few candidates. The first three buffers look rather promising (we have named and\r\ncolored them for the sake of clarity):\r\nFigure 7: Candidates for the encrypted buffer award\r\nThese three buffers are all referenced by the same function sub_180002ab8 that we have renamed to\r\ndecrypt_string_1 . This function looks like your typical string decryption function: it has numerous incoming\r\nreferences, as we can see below, each call with a different hardcoded parameter. There is a big chance that this\r\nparameter is a string index:\r\nhttps://malcat.fr/blog/writing-a-qakbot-50-config-extractor-with-malcat/\r\nPage 7 of 24\n\nFigure 8: The first string decryption function in context\r\nThe function decrypt_string_1 is rather simple: it calls an auxiliary function that we have named\r\ndecrypt_aes_plus_xor with our encrypted three buffers as parameter. Its decompiled code (F4) is presented\r\nbelow:\r\nvoid decrypt_string_1(xunknown4 string_index)\r\n{\r\n decrypt_aes_plus_xor(ENCRYPTED_STRINGS_1, 0x5ad, AES_ENCRYPTED_XOR_KEY, 0xd0, AES_PASSWORD, 0x63, string_ind\r\n return;\r\n}\r\nThe value of each variable is given below:\r\nName Address\r\nSize in\r\nbytes\r\nDescription\r\ndecrypt_strings_1 0x180002ab8 0x3f\r\nDecryption function for the first encrypted\r\nstrings array\r\nSTRINGS_1 0x1800282a0 0x5ad First encrypted strings array\r\nAES_ENCRYPTED_XOR_KEY 0x1800281c0 0xd0\r\nThe XOR key used to decrypt the string\r\narray, but AES256-CBC encrypted\r\nAES_PASSWORD 0x180028150 0x63\r\nThe password used to derive the AES256\r\nkey for AES_ENCRYPTED_XOR_KEY\r\ndecrypt_aes_plus_xor 0x18000dc2c 0x1de\r\nThe function that decrypts the string array\r\nand selects the string\r\naes_encrypt_decrypt_iv_prefix 0x180011504 0x3f7\r\nA function called by decrypt_aes_plus_xor\r\nthat decrypts or encrypts an arbitrary data\r\nbuffer using AES256 in CBC mode\r\nDecrypting the strings\r\nhttps://malcat.fr/blog/writing-a-qakbot-50-config-extractor-with-malcat/\r\nPage 8 of 24\n\nTo get what the function decrypt_aes_plus_xor does, a little reverse engineering is needed. As the code is\r\nrelatively short you can do it statically, although you will face some issues since APIs are resolved dynamically.\r\nTracing the function using a debugger is the smarter choice there. Anyway, at the end it is relatively easy, and the\r\nstring decryption routine looks something like that:\r\nFigure 9: How the strings are decrypted\r\nThe good news is that we have all the material we need already in Malcat! Indeed, Malcat already has a data\r\ntransform named CryptDeriveKey . And actually we don't even need it: what CryptDeriveKey does in this\r\nspecific configuration is just compute the SHA256 hash of the password and use it directly as key. As for\r\nCryptDecrypt : it is performing a simple AES 256 decryption in CBC mode, and we also have a transform for\r\nthis.\r\nNote: Advapi32.dll crypto functions add/remove padding by default, so make sur to check \"unpad\" in\r\nthe transform window\r\nSo using exclusively Malcat transforms, we can decrypt the strings manually in a few seconds, as demonstrated in\r\nthe GIF below:\r\nhttps://malcat.fr/blog/writing-a-qakbot-50-config-extractor-with-malcat/\r\nPage 9 of 24\n\nFigure 10: Decrypting th strings using Malcat transforms\r\nThe result is shown below:\r\nSOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\r\nProgramData\r\nnetstat -nao\r\n%s \"$%s = \\\"%s\\\"; \u0026 $%s\"\r\nnet localgroup\r\npowershell.exe\r\nroute print\r\n\"%s\\system32\\schtasks.exe\" /Create /ST %02u:%02u /RU \"NT AUTHORITY\\SYSTEM\" /SC ONCE /tr \"%s\" /Z /ET %02u:%02u /t\r\nComponent_08\r\nERROR: GetModuleFileNameW() failed with error: ERROR_INSUFFICIENT_BUFFER\r\nnet view\r\nipconfig /all\r\nSelf check\r\nT2X!wWMVH1UkMHD7SBdbgfgXrNBd(5dmRNbBI9\r\n4Lm7DW\u0026yMF*ELN4D8oNp0CtKUf*C2LAstORIBV\r\nStart screenshot\r\n%s.%u\r\nadrclient.dll\r\nnet share\r\nqwinsta\r\n\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\r\nat.exe %u:%u \"%s\" /I\r\nSelf test FAILED!!!\r\nComponent_07\r\nwhoami /all\r\n /c ping.exe -n 6 127.0.0.1 \u0026 type \"%s\\System32\\calc.exe\" \u003e \"%s\"\r\nhttps://malcat.fr/blog/writing-a-qakbot-50-config-extractor-with-malcat/\r\nPage 10 of 24\n\nerror res='%s' err=%d len=%u\r\nnltest /domain_trusts /all_trusts\r\n.lnk\r\ncmd\r\nschtasks.exe /Create /RU \"NT AUTHORITY\\SYSTEM\" /SC ONSTART /TN %u /TR \"%s\" /NP /F\r\n%s \\\"$%s = \\\\\\\"%s\\\\\\\\; \u0026 $%s\\\"\r\nERROR: GetModuleFileNameW() failed with error: %u\r\nschtasks.exe /Delete /F /TN %u\r\narp -a\r\nSelf check ok!\r\ncmd.exe /c set\r\n%s %04x.%u %04x.%u res: %s seh_test: %u consts_test: %d vmdetected: %d createprocess: %d\r\nMicrosoft\r\npowershell.exe -encodedCommand %S\r\nSELF_TEST_1\r\nmicrosoft.com,google.com,kernel.org,www.wikipedia.org,oracle.com,verisign.com,broadcom.com,yahoo.com,xfinity.com\r\nc:\\ProgramData\r\nnslookup -querytype=ALL -timeout=12 _ldap._tcp.dc._msdcs.%s\r\n%u;%u;%u;\r\npowershell.exe -encodedCommand\r\nrunas\r\n/teorema505\r\nSelf test OK.\r\nProfileImagePath\r\np%08x\r\nSadly there is no CNC address list nor any valuable configuration data there, beside the CNC http endpoint\r\n(/teorema505). So we'll have to dig deeper.\r\nDecrypting the second strings array\r\nThere is a second array of encrypted strings than can be found in this binary. This one is of lesser importance and\r\ncan be decrypted exactly the same way as the first array. The only difference is that a different pair of XOR key\r\nand AES password are used. If you are interested, below are the location of the variables relevant to the second\r\narray in our Qakbot sample:\r\nName Address\r\nSize in\r\nbytes\r\nDescription\r\ndecrypt_strings_2 0x18000de90 0x3f\r\nDecryption function for the second\r\nencrypted strings array\r\nSTRINGS_2 0x1800297a0 0x1836 Second encrypted strings array\r\nAES_ENCRYPTED_XOR_KEY_2 0x18002afe0 0xa0\r\nThe XOR key used to decrypt the string\r\narray, but AES256-CBC encrypted\r\nhttps://malcat.fr/blog/writing-a-qakbot-50-config-extractor-with-malcat/\r\nPage 11 of 24\n\nName Address\r\nSize in\r\nbytes\r\nDescription\r\nAES_PASSWORD_2 0x180029700 0x9f\r\nThe password used to derive the AES256\r\nkey for\r\nAES_ENCRYPTED_XOR_KEY_2\r\nIf you use the same process as for the first array, you should obtain the following strings list: see on pastebin.\r\nThe configuration\r\nLocating the configuration\r\nNow having the strings decrypted is all well and good, but our goal is to get the configuration of Qakbot, or at\r\nleast its list of command and control (CNC) servers. We will stay true to our process and start with data analysis.\r\nIn the list of decrypted strings presented last chapter, two strings look kind of unusual:\r\nThe 14th string at offset 0x182 in the string array: T2X!wWMVH1UkMHD7SBdbgfgXrNBd(5dmRNbBI9\r\nThe 15th string at offset 0x1a9 in the string array: 4Lm7DW\u0026yMF*ELN4D8oNp0CtKUf*C2LAstORIBV\r\nWe have seen in the previous chapter that the string decryption function decrypt_strings_1 takes as first\r\nparameter the index of the string to decrypt, that is its position relative to the start of the encrypted string array. So\r\nif we want to know how these two strings are used, we can just look for references to their offset in the code. Let\r\nus focus on the first string:\r\nFigure 11: Looking for references to string 0x182\r\nAnd we rapidely get two candidates: a function at offset 0x18000622c that we will call decrypt_CNC and one at\r\noffset 0x18000345c that we will call decrypt_params . And a second good news, both of these function\r\nreference a high-entropy buffer (named respectively CNC_LIST and PARAMS ) in addition to our 0x182 string.\r\nThe address of these functions and variables are given below:\r\nhttps://malcat.fr/blog/writing-a-qakbot-50-config-extractor-with-malcat/\r\nPage 12 of 24\n\nName Address\r\nSize in\r\nbytes\r\nDescription\r\ndecrypt_CNC 0x18000622c 0x2cc Decryption function for Qakbot's CNC\r\nCNC_LIST 0x180028852 0x51 Encrypted CNC list\r\ndecrypt_params 0x18000345c 0x76\r\nDecryption function for Qakbot's\r\ncampaign information\r\nPARAMS 0x180029022 0x51 Encrypted campaign informations\r\naes_decrypt_and_check_sha256 0x180015d14 0x105 Function to decrypt both encrypted blob\r\nAnd a last additional good news: both functions ultimately call our good old aes_encrypt_decrypt_iv_prefix .\r\nWe have already identified this function while reversing the string decryption process: it decrypts an AES256-\r\nCBC encrypted buffer prefixed by a 16 bytes IV.\r\nFigure 12: Cnc decryption function candidate\r\nDecrypting the CNC list\r\nIf we dig a bit deeper in the function, in particular in aes_decrypt_and_check_sha256 , we can see that the\r\nencrypted blobs CNC_LIST and PARAMS have a particular structure:\r\nThey are prefixed with their size (16 bits int)\r\nAfterwards comes a blob identifier, on one byte\r\nThen we get our already-known encrypted AES blob:\r\nA 16 bytes initialisation vector (IV)\r\nThe actual AES256-CBC encrypted content\r\nThe blob format is illustrated below:\r\nhttps://malcat.fr/blog/writing-a-qakbot-50-config-extractor-with-malcat/\r\nPage 13 of 24\n\nFigure 13: Cnc list encrypted blob\r\nTo decrypt the blob, we will use the same procedure as with the strings:\r\nCompute the SHA256 value of our password \"T2X!wWMVH1UkMHD7SBdbgfgXrNBd(5dmRNbBI9\"\r\n( 7085d1138cbac863a9b4f1bf85a4d413804ef3a3ec52729fa15747a6ee320325 )\r\nSelect the 0x40 bytes of AES encrypted data\r\nUse Malcat's transform AES decrypt in CBC mode, set the IV to the 16 bytes prefixing the encrypted data\r\nand the key to the sha256 hash\r\nDon't forget to check unpad\r\nAfter decrypting the CNC_LIST blob, we are facing a relatively simple binary structure. A bit of reversing in the\r\nfunction decrypt_CNC rapidly tells us everything we need to know to interpret it. The decrypted blob starts with a\r\nsha256 checksum, followed by a list of (ip, port) pairs. The details are given below:\r\nFigure 14: Cnc list decrypted\r\nAnd that's it! We got our 3 CnC addresses:\r\n31.210.173.10:443 (VT)\r\n185.156.172.62:443 (VT)\r\n185.113.8.123:443 (VT)\r\nNow let us see which kind of information we get with the second buffer PARAMS .\r\nDecrypting the campagn informations\r\nhttps://malcat.fr/blog/writing-a-qakbot-50-config-extractor-with-malcat/\r\nPage 14 of 24\n\nThe second referenced blob PARAMS is encrypted in the exact same way with the same password (the sha256 of\r\n\"T2X!wWMVH1UkMHD7SBdbgfgXrNBd(5dmRNbBI9\" ). If you reuse the same decryption process, you should get\r\nsomething like this at the end:\r\nFigure 15: Campaign infos decrypted\r\nWe get three parameters:\r\nThe parameter of id 10 seems to be the campaign ID (tchk08)\r\nThe parameter of id 3 seems to be a timestamp, most likely compilation time\r\nNo idea what parameter 40 is for\r\nAnd with this last piece of information, we will stop our search for Qakbot configuration data.\r\nScripting everything\r\nThe idea\r\nYou may have noticed, but decrypting everything was a bit tedious at the end. In this chapter, we will automate the\r\nprocess by writing a python configuration extractor in Malcat. Indeed, Malcat features powerful python bindings\r\nwhich are documented extensively. In a script, you have access to the complete analysis object in a somewhat\r\npythonic way.\r\nNote: if you own a full or pro version of Malcat, scripts can also be run from the command line in\r\nheadless mode\r\nThe idea behind the script is to redo all the steps that we have done manually:\r\nCollect all interesting referenced buffers in the .data section\r\nLook if these buffers are prefixed by their size. If not, try to infer the size by looking at constants used in\r\nreferencing functions code\r\nThen decrypt everything:\r\nFor strings arrays: try all possible triples permutations (strings_array, xor_key_encrypted,\r\naes_password) that we got to decrypt the strings and keep what works\r\nhttps://malcat.fr/blog/writing-a-qakbot-50-config-extractor-with-malcat/\r\nPage 15 of 24\n\nFor config extraction: use any high-entropy string found in the first string array as AES password\r\nand try to decrypt the CNC IPs and the campaign information. Keep what works (we can double-check with the sha256)\r\nThis approach (trying all possible keys) may seem not very subtle, but I have found out that string array ordering\r\nchanges from one sample to the next. I could have used code signatures in order to locate strings decryption\r\nfunctions more easily, but code may change and code signatures are not that robust against recompilation.\r\nAnalysing data on the other hand is a bit more robust and I hope the script will work for a while.\r\nSince this blog post is long enough, I will just leave you with the relatively well-documented code below. The\r\nonly notion that may be a bit foreign to you is Malcat's address space and its a2p functions and alike. But beside\r\nthis little detail, it should be rather easy to understand.\r\nThe script\r\n\"\"\"\r\nname: Qakbot 5.0\r\ncategory: config extractors\r\nauthor: malcat\r\nDecrypt strings and extract CnC informations from a (plain-text) Qakbot 5.0 sample\r\n\"\"\"\r\nimport malcat\r\nimport struct\r\nimport itertools\r\nimport hashlib\r\nimport json\r\nimport datetime\r\nimport re\r\nimport math\r\nimport collections\r\nfrom transforms.binary import CircularXor\r\nfrom transforms.block import AesDecrypt\r\n############################ utility functions\r\ndef decrypt_aes_iv_prefix(data:bytes, aes_password: bytes):\r\n key = hashlib.sha256(aes_password).digest()\r\n iv = data[0:16]\r\n data = data[16:]\r\n return AesDecrypt().run(data, mode=\"cbc\", iv=iv, key=key, unpad=True)\r\nhttps://malcat.fr/blog/writing-a-qakbot-50-config-extractor-with-malcat/\r\nPage 16 of 24\n\ndef get_all_referencing_functions(a:malcat.Analysis, address:int):\r\n res = []\r\n for incoming_ref_type, incoming_ref_address in a.xref[address]:\r\n fn = a.fns.find(incoming_ref_address)\r\n if fn is not None:\r\n res.append(fn)\r\n return set(res)\r\ndef entropy(data:str, base=2):\r\n if len(data) \u003c= 1:\r\n return 0\r\n counts = collections.Counter()\r\n for d in data:\r\n counts[d] += 1\r\n ent = 0\r\n probs = [float(c) / len(data) for c in counts.values()]\r\n for p in probs:\r\n if p \u003e 0.:\r\n ent -= p * math.log(p, base)\r\n return ent\r\n############################ interesting buffer heuristics\r\ndef enumerate_interesting_buffers(a:malcat.Analysis, section_name:str, prefixed_buffer:bool = False):\r\n section = a.map[section_name]\r\n # get all incoming xref in the section: denotates the start of a buffer\r\n data_xrefs = [x.address for x in a.xref[section.start:section.end]]\r\n for i in range(1, len(data_xrefs) - 1): # let's assume the first and last xrefs will never be interesting\r\n prev, cur, next = data_xrefs[i-1:i+2]\r\n prev_off = a.a2p(prev)\r\n cur_off = a.a2p(cur)\r\n next_off = a.a2p(next)\r\n if prefixed_buffer and cur - prev == 2:\r\n # is it a size-prefixed buffer ? (i.e. there is a referenced word 2 bytes before)\r\n size, = struct.unpack(\"\u003cH\", a.file[prev_off:cur_off])\r\n yield cur, size\r\n elif not prefixed_buffer:\r\n # we'll look for all immediate constants in referencing functions and see which one could be a size\r\n for fn in get_all_referencing_functions(a, cur):\r\n for basic_block in fn:\r\nhttps://malcat.fr/blog/writing-a-qakbot-50-config-extractor-with-malcat/\r\nPage 17 of 24\n\nif not basic_block.code:\r\n continue\r\n for instruction in basic_block:\r\n for operand in instruction:\r\n if operand.value and operand.value \u003e 0x10 and cur + operand.value \u003c= next and next -\r\n yield cur, operand.value\r\n############################ strings decryption\r\ndef get_potential_strings_triples(a:malcat.Analysis):\r\n # Here we will look for 3 buffers referenced from the same function:\r\n # one is the strings, one the xor key, one the aes password\r\n function_to_refs = {}\r\n done = set()\r\n # group all interesting buffers by referencing functions\r\n for address, size in enumerate_interesting_buffers(a, \".data\", prefixed_buffer=False):\r\n if size \u003c 0x20:\r\n continue\r\n # find all reference coming from functions\r\n for fn in get_all_referencing_functions(a, address):\r\n function_to_refs.setdefault(fn.address, []).append((address, size))\r\n # now try to find a function referencing 3 interesting buffers\r\n for fn_address, by_function in function_to_refs.items():\r\n if len(by_function) \u003c 3:\r\n # there should be at least 3 references to candidate buffers inside one function\r\n continue\r\n # we don't know which is one is the data, xor key or aes password: try all permutations of triples\r\n for candidate_triple in itertools.permutations(by_function, r=3):\r\n if not candidate_triple in done:\r\n done.add(candidate_triple)\r\n yield candidate_triple\r\ndef get_strings_arrays(a:malcat.Analysis):\r\n res = []\r\n # tries to decrypt all string arrays candidates\r\n for strings, xor, aes_password in get_potential_strings_triples(a):\r\n print(f\"Trying strings=({a.ppa(strings[0])}, {hex(strings[1])}), xor=({a.ppa(xor[0])}, {hex(xor[1])}), a\r\n try:\r\n # decrypt XOR key using AES\r\n xor_address, xor_size = xor\r\nhttps://malcat.fr/blog/writing-a-qakbot-50-config-extractor-with-malcat/\r\nPage 18 of 24\n\nxor_offset = a.a2p(xor_address)\r\n xor_buffer = a.file[xor_offset: xor_offset + xor_size]\r\n aes_address, aes_size = aes_password\r\n aes_offset = a.a2p(aes_address)\r\n aes_buffer = a.file[aes_offset: aes_offset + aes_size]\r\n xor_key = decrypt_aes_iv_prefix(xor_buffer, aes_buffer)\r\n # decrypt strings using XOR key\r\n strings_address, strings_size = strings\r\n strings_offset = a.a2p(strings_address)\r\n strings_buffer = a.file[strings_offset: strings_offset + strings_size]\r\n strings_decrypted = CircularXor().run(strings_buffer, key=xor_key).decode(\"utf8\")\r\n all_strings = strings_decrypted.split(\"\\x00\")\r\n res.append(all_strings)\r\n print(f\"Found {len(all_strings)} strings !\")\r\n except BaseException as e:\r\n print(f\"{e} :(\")\r\n return res\r\n############################ config extraction\r\ndef qakbot_config_extraction(a:malcat.Analysis):\r\n print(\"Running heuristic to find string arrays ...\")\r\n config_password = None\r\n strings_1 = []\r\n # find string arrays\r\n for string_array in get_strings_arrays(a):\r\n print(f\"\\nFound one string array of {len(string_array)} strings:\")\r\n print(\"\\n\".join(string_array))\r\n if \"ipconfig /all\" in string_array:\r\n strings_1 = string_array\r\n print()\r\n ips = []\r\n options = {}\r\n config_passwords = []\r\n # try to find endpoint\r\nhttps://malcat.fr/blog/writing-a-qakbot-50-config-extractor-with-malcat/\r\nPage 19 of 24\n\nfor s in strings_1:\r\n if re.match(r\"^/[a-zA-Z0-9_%?=\u0026-]{2,16}$\", s):\r\n options[\"http_endpoint\"] = s\r\n break\r\n # try to find password candidates: high-entropy, good length, not a lot of space or backslaches\r\n for s in strings_1:\r\n if len(s) \u003e 30 and len(s) \u003c 60 and entropy(s) \u003e 4 and s.count(\" \") \u003c 2 and s.count(\"\\\\\") \u003c 2:\r\n config_passwords.append(s)\r\n print(f\"Found {len(config_passwords)} password candidates: {', '.join(config_passwords)}\")\r\n # ok now try to look for prefixed buffers:\r\n for address, size in enumerate_interesting_buffers(a, \".data\", prefixed_buffer=True):\r\n # and try to decrypt using our password candidates\r\n for config_password in config_passwords:\r\n print(f\"Trying config decryption for {a.ppa(address)}, {hex(size)}) with password {config_password}\r\n try:\r\n offset = a.a2p(address)\r\n buffer = a.file[offset:offset+size]\r\n # AES decrypt the buffer (skip blob identifer)\r\n decrypted = decrypt_aes_iv_prefix(buffer[1:], config_password.encode(\"ascii\"))\r\n # verify checksum\r\n checksum = decrypted[:32]\r\n data = decrypted[32:]\r\n if hashlib.sha256(data).digest() != checksum:\r\n raise ValueError(\"Invalid blob checksum\")\r\n # looks like campaign info?\r\n if data.count(b\"=\") \u003e= 2:\r\n data = data.decode(\"ascii\").replace(\"\\r\", \"\")\r\n d = dict([x.split(\"=\") for x in data.split(\"\\n\") if x.strip()])\r\n print(f\"Found config dictionnary with {len(d)} entries!\")\r\n for k, v in d.items():\r\n if k == \"10\":\r\n k = \"campaign_id\"\r\n elif k == \"3\":\r\n k = \"date\"\r\n v = datetime.datetime.fromtimestamp(int(v)).isoformat()\r\n options[k] = v\r\n # looks like campaign IPs list?\r\n elif data.startswith(b\"\\x01\"):\r\n for i in range(0, len(data), 8):\r\n type, ip, port,_ = struct.unpack_from(\"\u003eB4sHB\", data, i)\r\nhttps://malcat.fr/blog/writing-a-qakbot-50-config-extractor-with-malcat/\r\nPage 20 of 24\n\nif type != 1:\r\n raise ValueError(f\"Unknown CNC format {type}\")\r\n ip = \".\".join(map(str, struct.unpack(\"BBBB\", ip)))\r\n ips.append((ip, port))\r\n print (\"Found IPs !\")\r\n else:\r\n print(\"Unknwon config data\")\r\n except Exception as e:\r\n print(f\"{e} :(\")\r\n return {\r\n \"cncs\": ips,\r\n \"options\": options,\r\n }\r\n################################ MAIN\r\nif __name__ == \"__main__\":\r\n config = qakbot_config_extraction(analysis)\r\n print(\"\\nQAKBOT_CONFIG = \", end=\"\")\r\n print(json.dumps(config, indent=4))\r\nResult\r\nAgainst the analyzed sample\r\nWhen run against the last stage cldapi.dll , the script will output something like this:\r\nRunning heuristic to find string arrays ...\r\nTrying strings=(0x180028150 (.data:150), 0x63), xor=(0x180028150 (.data:150), 0x63), aes_password=(0x180028150 (\r\nTrying strings=(0x180028150 (.data:150), 0x63), xor=(0x180028150 (.data:150), 0x63), aes_password=(0x180028150 (\r\nTrying strings=(0x1800297a0 (.data:17a0), 0x1836), xor=(0x18002afe0 (.data:2fe0), 0xa0), aes_password=(0x1800297\r\nTrying strings=(0x18002afe0 (.data:2fe0), 0xa0), xor=(0x18002afe0 (.data:2fe0), 0xa0), aes_password=(0x1800297a0\r\n...\r\nTrying strings=(0x18002afe0 (.data:2fe0), 0xa0), xor=(0x18002afe0 (.data:2fe0), 0xa0), aes_password=(0x18002afe0\r\nTrying strings=(0x18002b190 (.data:3190), 0x9c0), xor=(0x18002b190 (.data:3190), 0x9c0), aes_password=(0x18002b1\r\nFound one string array of 52 strings:\r\nSOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\r\nProgramData\r\nnetstat -nao\r\n%s \"$%s = \\\"%s\\\"; \u0026 $%s\"\r\nhttps://malcat.fr/blog/writing-a-qakbot-50-config-extractor-with-malcat/\r\nPage 21 of 24\n\n...\r\nFound one string array of 185 strings:\r\n%SystemRoot%\\SysWOW64\\xwizard.exe\r\n.dat\r\nkernelbase.dll\r\nWBJ_IGNORE\r\nmpr.dll\r\n...\r\nFound 2 password candidates: T2X!wWMVH1UkMHD7SBdbgfgXrNBd(5dmRNbBI9, 4Lm7DW\u0026yMF*ELN4D8oNp0CtKUf*C2LAstORIBV\r\nTrying config decryption for 0x180028852 (.data:852), 0x51) with password T2X!wWMVH1UkMHD7SBdbgfgXrNBd(5dmRNbBI9\r\nTrying config decryption for 0x180028852 (.data:852), 0x51) with password 4Lm7DW\u0026yMF*ELN4D8oNp0CtKUf*C2LAstORIBV\r\nTrying config decryption for 0x180029022 (.data:1022), 0x51) with password T2X!wWMVH1UkMHD7SBdbgfgXrNBd(5dmRNbBI\r\nTrying config decryption for 0x180029022 (.data:1022), 0x51) with password 4Lm7DW\u0026yMF*ELN4D8oNp0CtKUf*C2LAstORIB\r\nQAKBOT_CONFIG = {\r\n \"cncs\": [\r\n [\r\n \"31.210.173.10\",\r\n 443\r\n ],\r\n [\r\n \"185.156.172.62\",\r\n 443\r\n ],\r\n [\r\n \"185.113.8.123\",\r\n 443\r\n ]\r\n ],\r\n \"options\": {\r\n \"http_endpoint\": \"/teorema505\",\r\n \"campaign_id\": \"tchk08\",\r\n \"40\": \"1\",\r\n \"date\": \"2024-01-31T15:22:34\"\r\n }\r\n}\r\nIt works!\r\nAgainst another sample\r\nBut does the extractor script work with other samples too? Let us try with another unpacked Qakbot sample found\r\non Malpedia:\r\nhttps://malcat.fr/blog/writing-a-qakbot-50-config-extractor-with-malcat/\r\nPage 22 of 24\n\nRunning heuristic to find string arrays ...\r\nTrying strings=(0x140028150 (.data:150), 0x80), xor=(0x140028150 (.data:150), 0x80), aes_password=(0x1400281e0 (\r\nTrying strings=(0x140028150 (.data:150), 0x80), xor=(0x140028150 (.data:150), 0x80), aes_password=(0x140028280 (\r\nTrying strings=(0x140028150 (.data:150), 0x80), xor=(0x1400281e0 (.data:1e0), 0x94), aes_password=(0x140028150 (\r\nTrying strings=(0x140028150 (.data:150), 0x80), xor=(0x1400281e0 (.data:1e0), 0x94), aes_password=(0x1400281e0 (\r\nTrying strings=(0x140029620 (.data:1620), 0x1825), xor=(0x1400294c0 (.data:14c0), 0xc0), aes_password=(0x1400295\r\n...\r\nTrying strings=(0x14002b220 (.data:3220), 0x9c0), xor=(0x14002b220 (.data:3220), 0x9c0), aes_password=(0x14002b2\r\nFound one string array of 52 strings:\r\nComponent_08\r\nSelf test FAILED!!!\r\nroute print\r\nwhoami /all\r\n...\r\nFound one string array of 185 strings:\r\nkernelbase.dll\r\nmcshield.exe\r\nwmic process call create 'expand \"%S\" \"%S\"'\r\nSOFTWARE\\Microsoft\\Windows Defender\\Exclusions\\Paths\r\n%ProgramFiles%\\Internet Explorer\\iexplore.exe\r\n%SystemRoot%\\SysWOW64\\xwizard.exe\r\n...\r\nFound 2 password candidates: 4Lm7DW\u0026yMF*ELN4D8oNp0CtKUf*C2LAstORIBV, ArpBUw9Lb9ndqXhFTfBst9YHotv92LB7BKvK#ewZn@@\r\nTrying config decryption for 0x140028842 (.data:842), 0x61) with password 4Lm7DW\u0026yMF*ELN4D8oNp0CtKUf*C2LAstORIBV\r\nTrying config decryption for 0x140028842 (.data:842), 0x61) with password ArpBUw9Lb9ndqXhFTfBst9YHotv92LB7BKvK#e\r\nTrying config decryption for 0x140029012 (.data:1012), 0x51) with password 4Lm7DW\u0026yMF*ELN4D8oNp0CtKUf*C2LAstORIB\r\nTrying config decryption for 0x140029012 (.data:1012), 0x51) with password ArpBUw9Lb9ndqXhFTfBst9YHotv92LB7BKvK#\r\nQAKBOT_CONFIG = {\r\n \"cncs\": [\r\n [\r\n \"146.70.158.28\",\r\n 6882\r\n ],\r\n [\r\n \"116.202.110.87\",\r\n 443\r\n ],\r\n [\r\n \"77.73.39.175\",\r\n 32103\r\n ],\r\nhttps://malcat.fr/blog/writing-a-qakbot-50-config-extractor-with-malcat/\r\nPage 23 of 24\n\n[\r\n \"185.156.172.62\",\r\n 443\r\n ],\r\n [\r\n \"185.117.90.142\",\r\n 6882\r\n ]\r\n ],\r\n \"options\": {\r\n \"http_endpoint\": \"/teorema505\",\r\n \"campaign_id\": \"bmw01\",\r\n \"date\": \"2024-01-26T12:25:33\"\r\n }\r\n}\r\nIt works too! Note how the strings inside the two strings arrays are ordered differently from one sample to another.\r\nConclusion\r\nIn this blog post we have learnt how to leverage Malcat's file parsers and data transforms to unpack a multilayered\r\nMSI installer up to the final Qakbot sample. Sticking to pure static analysis, and with heavy emphasis on data\r\nanalysis, we have seen how to decrypt Qakbot's string arrays and decode its command and control configuration.\r\nFinally, by making use of Malcat's python bindings, we have written a fully functional static configuration\r\nextractor. The extractor script does not use any code signature nor any hardcoded value, which should make it\r\nhopefully robust to future changes.\r\nI hope that you enjoyed this unpacking/scripting session. Hopefully, you'll find the Qakbot configuration extractor\r\nuseful for your future analyses. As usual, feel free to share with us your remarks or suggestions!\r\nSource: https://malcat.fr/blog/writing-a-qakbot-50-config-extractor-with-malcat/\r\nhttps://malcat.fr/blog/writing-a-qakbot-50-config-extractor-with-malcat/\r\nPage 24 of 24",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://malcat.fr/blog/writing-a-qakbot-50-config-extractor-with-malcat/"
	],
	"report_names": [
		"writing-a-qakbot-50-config-extractor-with-malcat"
	],
	"threat_actors": [],
	"ts_created_at": 1775434168,
	"ts_updated_at": 1775791270,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/8ac5a064eaf8c4cd0215f571f14011a9aeb6cba4.pdf",
		"text": "https://archive.orkl.eu/8ac5a064eaf8c4cd0215f571f14011a9aeb6cba4.txt",
		"img": "https://archive.orkl.eu/8ac5a064eaf8c4cd0215f571f14011a9aeb6cba4.jpg"
	}
}