{
	"id": "02108bf4-7d8f-43eb-9f75-c70e911131a9",
	"created_at": "2026-04-06T01:28:56.541148Z",
	"updated_at": "2026-04-10T03:20:00.841546Z",
	"deleted_at": null,
	"sha1_hash": "455d298a8ec42691186cbf8890ae708ff03ad33e",
	"title": "grap: Automating QakBot strings decryption",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 632986,
	"plain_text": "grap: Automating QakBot strings decryption\r\nArchived: 2026-04-06 00:28:12 UTC\r\nPublished: September 10, 2020\r\nOur last grap post demonstrated on how to use grap to create and find patterns within QakBot samples.\r\nThis post focuses on QakBot’s documented strings decryption feature:\r\nCreate patterns to find the function where it is implemented\r\nExtract relevant variables (decryption key…)\r\nAutomate decryption:\r\nwithin IDA\r\nas a standalone script using pefile and grap bindings\r\nReferences\r\n[1] - Reversing Qakbot - https://hatching.io/blog/reversing-qakbot/\r\n[2] - Deep Analysis of QBot Banking Trojan - https://n1ght-w0lf.github.io/malware%20analysis/qbot-banking-trojan/#encrypted-strings\r\n[3] - Malware Analysis: Qakbot [Part 2] - https://darkopcodes.wordpress.com/2020/06/07/malware-analysis-qakbot-part-2/\r\n1 - Decryption function\r\nQakBot’s strings are encrypted using a simple XOR cipher with a repeating key [2].\r\nLooking into a decryption implementation [2] we find the operation idx \u0026 0x3F (the key size is 0x40=64), let’s\r\nuse grap to find it with IDA bindings ( opcode is 'and' and arg2 is 0x3f ):\r\nhttps://quosecgmbh.github.io/blog/grap_qakbot_strings.html\r\nPage 1 of 11\n\nTwo (out of four) matches are in the same function that implements strings decryption. The decryption function\r\ntakes an offset as argument, decrypts the ciphertext at this offset, and returns the plaintext.\r\nA first pattern to detect this decryption function focuses on the and and xor operations within a loop (see\r\nqakbot_strings_3f.grapp):\r\n2 - Parsing variables\r\nhttps://quosecgmbh.github.io/blog/grap_qakbot_strings.html\r\nPage 2 of 11\n\nWe have found the decryption function, we can use grap to create a more precise pattern. As we want to automate\r\ndecryption we have to find function variables, including:\r\nAddress and size of the ciphertext\r\nAddress and size of the keystream\r\n2.1 - Pattern creation\r\nLet’s create an initial pattern with the IDA plugin :\r\nWe used the UI to create a pattern matching exactly the instructions containing the variables. Let’s edit it to:\r\nMatch when the variables are different (match on opcode for instance)\r\nOutput the instructions where variables are defined with getid\r\nThe parsing pattern for the decryption function is (see qakbot_strings_parsing.grapp):\r\nhttps://quosecgmbh.github.io/blog/grap_qakbot_strings.html\r\nPage 3 of 11\n\n2.2 - Variables across samples\r\nWe can use grep to output matching instructions from samples with the wanted decryption function:\r\n$ grap qakbot_strings_parsing.grapp *.grapcfg | grep CIPHERSIZE1\r\n1_CIPHERSIZE1: 0x5f09cb, push 0x8a3\r\n1_CIPHERSIZE1: 0x5f15cb, push 0x8a3\r\n1_CIPHERSIZE1: 0x403537, push 0x1567\r\n1_CIPHERSIZE1: 0x590ccfa, push 0x14d0\r\n1_CIPHERSIZE1: 0x2ff87af, push 0xa8e\r\nhttps://quosecgmbh.github.io/blog/grap_qakbot_strings.html\r\nPage 4 of 11\n\n1_CIPHERSIZE1: 0x1cd90a, push 0x14d0\r\n1_CIPHERSIZE1: 0x2ed238c, push 0xa45\r\n1_CIPHERSIZE1: 0x7aa7aff, push 0xa8e\r\n1_CIPHERSIZE1: 0x403537, push 0x1567\r\n[...]\r\nWe find that:\r\nCIPHERSIZE varies across samples\r\nKEYSIZE is always 0x40\r\n3 - Matching the whole function\r\nWe want to study calls to the decryption function. For this we need to find its entrypoint.\r\nBack into our sample, the created pattern does not begin at the function entrypoint but there is only one basic\r\nblock in between:\r\nThis basic block can be matched easily:\r\nMatch the first instruction based on multiple incoming nodes (the decryption function is called many\r\ntimes): nfathers\u003e=5\r\nMatch the remainder of the basic block with repeat=* ( lazyrepeat=false )\r\nThis can be included into the previous pattern (see qakbot_strings_parsing_func.grapp):\r\nhttps://quosecgmbh.github.io/blog/grap_qakbot_strings.html\r\nPage 5 of 11\n\n4 - Matching function calls\r\nThe decryption function is called with its argument (offset within the ciphertext buffer) pushed on the stack. Most\r\nof the time the push is immediately followed by the call but this is not always the case:\r\n \r\nAt this point we know the function entrypoint. So instead of extending the same pattern again, we can use a\r\npattern template using a placeholder for the entrypoint (it will have to be replaced by a script):\r\n5 - Automate decryption\r\nWe now have the necessary patterns to automate strings decryption in an IDA script:\r\nFind and parse function variables\r\nFind calls to the decryption function and parse pushed arguments\r\n5.1 - Parsing the function variables\r\nThe python bindings give access to the matched instructions through the named defined by getid .\r\nSome of the parsed information is structured (opcode, arg1, arg2, address…) but many fields are simply strings\r\n(arg1, arg2 for instance) so you will need some parsing to get the right information. The bindings provide a few\r\nparsers:\r\nhttps://quosecgmbh.github.io/blog/grap_qakbot_strings.html\r\nPage 6 of 11\n\nparse_first_immediate: returns the first integer found in an immediate construct( 0x3f returns 0x3f, 43\r\nreturns 43, 0x23+eax return None )\r\nparse_first_indirect: returns the first integer in an indirect construct ( [0x394173e] returns 0x394173e,\r\n[0x394173e+ax] returns None )\r\nparse_first_address: returns the first hex found integer (with 0x), regardless of the construct ( 0x3f returns\r\n0x3f, 0x23+eax return 0x23, [0x394173e+ax] returns 0x394173e, 43 returns None )\r\nparse_int_hex: converts a string representing an integer ( 0x3f , 43 ) into its integer counterpart\r\nimport pygrap\r\npattern_function = \"...\" # see qakbot_strings_parsing_func.grapp\r\ndef parse_decrypt_function():\r\nmatches = pygrap.ida_match(pattern_function, print_matches=False)\r\nif \"qakbot_strings_parsing_func\" in matches:\r\nfor match in matches[\"qakbot_strings_parsing_func\"]:\r\nfunc_addr = match[\"0_EP\"][0].info.address\r\nsize1 = pygrap.parse_first_immediate(match[\"1_CIPHERSIZE1\"][0].info.arg1)\r\ncipher_addr = pygrap.parse_first_immediate(match[\"2_CIPHER\"][0].info.arg1)\r\nsize2 = pygrap.parse_first_immediate(match[\"3_CIPHERSIZE2\"][0].info.arg2)\r\nxorkey_addr = pygrap.parse_first_address(match[\"5_XORKEY\"][0].info.arg2)\r\n \r\nif size1 != size2 + 1:\r\nprint(\"ERROR: size1 and size2 do not match\")\r\nreturn\r\nprint(\"Decryption function at:\", hex(func_addr))\r\nprint(\"XOR key at:\", hex(xorkey_addr))\r\nprint(\"XOR key:\", idc.get_bytes(xorkey_addr, 0x40).hex())\r\nprint(\"Cipher block at:\", hex(cipher_addr))\r\nprint(\"Cipher block size:\", hex(size1))\r\n \r\nreturn func_addr, xorkey_addr, cipher_addr, size1\r\nRan into IDA, the script parses the decryption function:\r\n5.2 - Decrypting strings\r\nWe now have the function variables and can, for each function call, decrypt the corresponding string and add a\r\ncomment within IDA.\r\npattern_call=\"\"\"\r\ndigraph push_call_func {\r\nhttps://quosecgmbh.github.io/blog/grap_qakbot_strings.html\r\nPage 7 of 11\n\npush [cond=\"opcode is push\", getid=PUSH]\r\nnotpush [cond=\"not(opcode is push)\", minrepeat=0, maxrepeat=5, lazyrepeat=true]\r\ncall [cond=\"opcode is call\"]\r\nfunc [cond=\"address == FILL_ADDRESS\"]\r\n \r\npush -\u003e notpush\r\nnotpush -\u003e call\r\ncall -\u003e func [childnumber=2]\r\n}\r\n\"\"\"\r\ndef decrypt_strings():\r\nfunc_addr, xorkey_addr, cipher_addr, cipher_size = parse_decrypt_function()\r\npattern_call_final = pattern_call.replace(\"FILL_ADDRESS\", hex(func_addr))\r\nmatches = pygrap.ida_match(pattern_call_final, print_matches=False)\r\n \r\nif \"push_call_func\" in matches:\r\nfor match in matches[\"push_call_func\"]:\r\npush_inst = match[\"PUSH\"][0]\r\npush_addr = push_inst.info.address\r\noffset = pygrap.parse_first_immediate(push_inst.info.arg1)\r\nif offset:\r\ndec = decrypt_string(offset, xorkey_addr, cipher_addr, cipher_size)\r\nprint(hex(push_addr), hex(offset), dec)\r\nidc.set_cmt(push_addr, dec, 1)\r\nThe decryption itself is a XOR stream cipher:\r\ndef decrypt_string(offset, xorkey_addr, cipher_addr, cipher_size):\r\nif offset \u003e= cipher_size:\r\nreturn\r\nres = \"\"\r\nwhile offset \u003c cipher_size - 1:\r\ncipher_b = idc.get_bytes(cipher_addr+offset, 1)[0]\r\nkey_b = idc.get_bytes(xorkey_addr + (offset\u00260x3F), 1)[0]\r\nc = cipher_b ^ key_b\r\nif c == 0:\r\nbreak\r\nres += chr(c)\r\noffset += 1\r\nreturn res\r\nAs expected the script appends decrypted strings as a comment:\r\nhttps://quosecgmbh.github.io/blog/grap_qakbot_strings.html\r\nPage 8 of 11\n\nThe full IDA script can found here: qakbot_strings_decrypt_IDA.py\r\n5.3 - Standalone script\r\nThe script needs some change for it to run without IDA:\r\nReplace IDA functions with pefile alternatives: idc.get_bytes -\u003e pe.get_data\r\nReplace grap IDA helpers with generic bindings: pygrap.ida_match -\u003e pygrap.match_graph\r\nAdd a few checks to handle errors\r\nOut of IDA grap does the disassembly itself, when possible it will rely on an existing .grapcfg file:\r\n bin_path = sys.argv[1]\r\n dot_path = sys.argv[1] + \".grapcfg\"\r\n #use_existing specifies wether an existing dot file should be used unchanged or overwritten\r\n pygrap.disassemble_file(bin_path=bin_path, dot_path=dot_path, use_existing=True)\r\n data = open(bin_path, \"rb\").read()\r\n pe = pefile.PE(data=data)\r\n pe_baseaddr = pe.OPTIONAL_HEADER.ImageBase\r\n \r\n pe_cfg = pygrap.getGraphFromPath(dot_path)\r\n decrypt_strings(pe, pe_baseaddr, pe_cfg)\r\nThe full script can found here: qakbot_strings_decrypt.py\r\nRunning it on all the unpacked samples reveals further IOCs:\r\n$ for i in *_unpacked; do python3 qakbot_strings_decrypt.py $i; done\r\n---\r\nSample: s01_unpacked\r\nDecryption function at: 0x590cce9\r\nXOR key at: 0x5920df0\r\nhttps://quosecgmbh.github.io/blog/grap_qakbot_strings.html\r\nPage 9 of 11\n\nXOR key: 10b5f45c25f213946f2a5c504fc091ec4405de29ceb1ec0190997a0ccb6db09ca0fb043a40d1eaab79b02226b6aeede2ef7ae2a\r\nCipher block at: 0x591c320\r\nCipher block size: 0x14d0\r\nStrings:\r\n0x58f1211 0x386 SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run\r\n0x58f287d 0x1499 netteller.com\r\n0x58f5683 0x30b injects_disabled\r\n0x58f6523 0x7c0 %%%BOT_NICK%%%\r\n0x591345c 0x2c4 c:\\pagefile.sys.bak2.txt\r\n[...]\r\n---\r\nSample: s03_unpacked\r\nDecryption function at: 0x2ff7b9e\r\nXOR key at: 0x300b020\r\nXOR key: 1f0bae1ea5621beab6b7e62939131e95b938e527c399152d4367c2fa20d50a86e93d4a02d0502fadbceec2ddc7e4e4b2dc1a473\r\nCipher block at: 0x3007be8\r\nCipher block size: 0xa8e\r\nStrings:\r\n0x2fe286f 0xa7 cashmanagementconnectionstring\r\n0x2fe52f8 0x87 rapportgp\r\n0x2fe6507 0x203 %%%BOT_NICK%%%\r\n0x2fe9ae2 0x649 /perl/test/gw2.pl\r\n0x2fed0dd 0x280 Administrator\r\n[...]\r\nConclusion\r\nWe demonstrated how to use grap (through python bindings and the IDA plugin) to find, detect and parse\r\nQakBot’s documented string decryption function.\r\ngrap can be used to automate the parsing of the function’s variables and arguments. The bindings were used to\r\nfind and decrypt the obfuscated strings, either within IDA or as a standalone script using pefile .\r\nResources\r\nMore documentation on grap can be found here:\r\nInstall (Linux): https://github.com/QuoSecGmbH/grap/#installation\r\nInstall (Windows): https://github.com/QuoSecGmbH/grap/blob/master/WINDOWS.md\r\nInstall (IDA plugin): https://github.com/QuoSecGmbH/grap/blob/master/IDA.md\r\nPattern syntax: https://github.com/QuoSecGmbH/grap/releases/download/v1.1.0/grap_graphs.pdf\r\nSyntax highlighting (vim): https://github.com/QuoSecGmbH/grap/blob/master/doc/syntax_highlighting.md\r\nhttps://quosecgmbh.github.io/blog/grap_qakbot_strings.html\r\nPage 10 of 11\n\nSource: https://quosecgmbh.github.io/blog/grap_qakbot_strings.html\r\nhttps://quosecgmbh.github.io/blog/grap_qakbot_strings.html\r\nPage 11 of 11",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://quosecgmbh.github.io/blog/grap_qakbot_strings.html"
	],
	"report_names": [
		"grap_qakbot_strings.html"
	],
	"threat_actors": [],
	"ts_created_at": 1775438936,
	"ts_updated_at": 1775791200,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/455d298a8ec42691186cbf8890ae708ff03ad33e.pdf",
		"text": "https://archive.orkl.eu/455d298a8ec42691186cbf8890ae708ff03ad33e.txt",
		"img": "https://archive.orkl.eu/455d298a8ec42691186cbf8890ae708ff03ad33e.jpg"
	}
}