{
	"id": "9395cf00-d1dd-4a87-8772-9a2fbd804298",
	"created_at": "2026-04-06T02:10:36.688087Z",
	"updated_at": "2026-04-10T03:19:58.921318Z",
	"deleted_at": null,
	"sha1_hash": "67bd2633e208bae0f4ab3e2f747adf06b82a199e",
	"title": "Automated dynamic import resolving using binary emulation « lopqto's adventures",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 394486,
	"plain_text": "Automated dynamic import resolving using binary emulation «\r\nlopqto's adventures\r\nBy Hamidreza Babaee Poking around to find out. GitHub X/Twitter\r\nArchived: 2026-04-06 01:32:18 UTC\r\nAnalyzing malwares is often not an easy task because there are lots of tricks and techniques that malwares use to\r\nevade detection and classification or to make the post-analysis more difficult. One such trick is to resolve\r\nwindows API calls dynamically (called “dynamic import resolving”).\r\nIn this blog post, we will talk about dynamic import resolving and a pattern to detect it when reversing malwares,\r\nhow to defeat this trick using binary emulation and Qiling framework (resolve API calls and extract function\r\nnames), and finally we will integrate our emulation framework with Ghidra.\r\nIn the last section, we will talk about a solution to run Python version 3 and Qiling trough Ghidra so we can see\r\nthe result of our script inside the decompiler/disassembler view. It will make post-analysis easier.\r\nAs a real-life example, we will analyze Netwalker which used this technique and we will discuss our idea around\r\nthat sample.\r\nWhat is dynamic import resolving\r\nLet’s talk about dynamic import resolving and indirect function calls. It’s a common technique that malwares use\r\nto hide their intention, make the static analysis more difficult, bypass some red flags, etc.\r\nIn this technique, the malware tries to create an IAT (Import Address Table) during the execution so there is no\r\nsign of used API calls in the PE header.\r\nThis technique often shows up in a specific pattern; At the beginning of the execution, the program will build an\r\narray of function pointers which works like an IAT and the malware can use stored function pointers with indirect\r\ncalls as shown below:\r\nhttps://lopqto.me/posts/automated-dynamic-import-resolving\r\nPage 1 of 13\n\nIt’s rather difficult to determine which function would be called by these indirect function calls without actually\r\nexecuting the binary.\r\nTo dynamically make a function pointer, the two API calls LoadLibraryA() and GetProcAddress() are often\r\nused.\r\nAccording to the Microsoft docs, LoadLibraryA() :\r\nLoads the specified module into the address space of the calling process. The specified module may\r\ncause other modules to be loaded.\r\nHMODULE LoadLibraryA(\r\n LPCSTR lpLibFileName\r\n);\r\nAnd GetProcAddress() :\r\nRetrieves the address of an exported function or variable from the specified dynamic-link library\r\n(DLL).\r\nFARPROC GetProcAddress(\r\n HMODULE hModule,\r\n LPCSTR lpProcName\r\n);\r\nLook at this pseudo-code as a demonstration:\r\ntypedef ret_type (__stdcall *f_func)(param_a, param_b);\r\nHINSTANCE hLibrary = LoadLibrary(\"ntdll.dll\");\r\nf_func LocalNtCreateFile = (f_func)GetProcAddress(hLibrary, \"NtCreateFile\");\r\nhttps://lopqto.me/posts/automated-dynamic-import-resolving\r\nPage 2 of 13\n\nLocalNtCreateFile is a function pointer which points to NtCreateFile , which can be stored in an array a.k.a\r\nIAT.\r\nTo make things more spicy, sometimes malware authors also encrypt the strings passed to LoadLibrary() and\r\nGetProcAddress() like what Netwalker did. It will be near to impossible to analyze malware without solving this\r\nproblem first.\r\nChoosing the approach\r\nTo solve these types of techniques and tricks there are a few approaches. For example, we can sometimes decrypt\r\npassed strings statically or we can develop an IDA plugin (or any disassembler and decompiler that supports\r\nplugins) but that would be a rather time-consuming task. Alternatively, we can use debuggers to execute the\r\nmalware step by step, and rename variables according to dynamically resolved functions but this is a lot of\r\nrepetition.\r\nI chose binary emulation because it gives us the best of both worlds, We can have the power of automation and the\r\nease of debugging. It’s worth mentioning that emulating can be very slow at times, especially when dealing with\r\nencryption and decryption algorithms. Personally, I think this is an acceptable trade-off.\r\nFor binary emulation we will use Qiling. Read my previous post to see why.\r\nAnalyzing Netwalker\r\nToday’s sample is NetWalker link! . Netwalker used dynamic import resolving technique with encrypted strings so\r\nit is a good example for us to demonstrate our idea and approach around that.\r\nAs discussed before, most of the time malwares will try to build an IAT at the beginning of the execution - and\r\nNetWalker does this.\r\nAfter disassembling the malware, we can see a function call right after the entry .\r\nhttps://lopqto.me/posts/automated-dynamic-import-resolving\r\nPage 3 of 13\n\nJumping to that function, we can see the pattern mentioned above; A function is called multiple times and the\r\nreturn value is stored in an array.\r\nhttps://lopqto.me/posts/automated-dynamic-import-resolving\r\nPage 4 of 13\n\nThis pattern is a sign of dynamic import resolving. We can confirm our guess with a debugger like below:\r\nLet’s jump to the code and write a script to extract these function names.\r\nhttps://lopqto.me/posts/automated-dynamic-import-resolving\r\nPage 5 of 13\n\nI’ve discussed the basics of the Qiling like hook_code() and ql.mem.read in the previous post.\r\nIn such scenarios, we don’t need to emulate the entire malware, we just need to execute the dynamic import table\r\nresolution bit. So we need to find the start and the end of that section. This is rather easy because our target is\r\ninside a function, so we only need to emulate that specific function.\r\nql.run(begin=0x0040c1a0, end=0x0040c1a5)\r\nIn this process of analyzing malwares with binary emulation, you need only be creative. For example, in this\r\nsample, there are plenty of approaches that you can use; however I chose the easiest and fastest (specifically\r\ndevelopment time, this solution performs rather badly).\r\nLet’s talk about the approach. As you can see in the image below, the return value of the (probably) decrypter and\r\nresolver function is stored in the eax register and then moved to dword ptr [ecx + int] . So we just need to\r\nhook the code and extract the value of eax in the right location.\r\nhttps://lopqto.me/posts/automated-dynamic-import-resolving\r\nPage 6 of 13\n\nWe can run the emulator and try to hook_code() to catch every instruction that is going to be executed.\r\nql.hook_code(extract_eax)\r\nAs you may notice, extract_eax() is a callback function that is designed to extract the value of eax . Qiling\r\nwill pass the ql (sandbox) object, the address and the size of the instruction to this callback function.\r\nWe can extract the instruction inside extract_eax() with mem.read() as below:\r\nbuf = ql.mem.read(address, size)\r\nbuf is a Python bytearray of our instruction. The next step is detecting the right location to extract eax . By\r\nlooking at the disassembler we can see a pattern. the first part of the opcode is similar.\r\nhttps://lopqto.me/posts/automated-dynamic-import-resolving\r\nPage 7 of 13\n\nNext if will detect the right location:\r\nif \"8941\" in buf.hex():\r\nto extract eax value we need to do this:\r\neax_value = ql.reg.eax\r\neax_value is an address that points to an API call. We can search that address inside import_symbols to extract\r\nthe API name.\r\nfunc = ql.loader.import_symbols[eax_value]\r\nfunc_dll = func[\"dll\"]\r\nfunc_name = func[\"name\"].decode(\"ascii\")\r\nprint(f\"found {func_dll}.{func_name} at {hex(address)}\")\r\nFulll code will be:\r\ndef extract_eax(ql, address, size):\r\n buf = ql.mem.read(address, size)\r\nhttps://lopqto.me/posts/automated-dynamic-import-resolving\r\nPage 8 of 13\n\nif \"8941\" in buf.hex(): # dword ptr [ECX + hex],EAX\r\n eax_value = ql.reg.eax\r\n func = ql.loader.import_symbols[eax_value]\r\n func_dll = func[\"dll\"]\r\n func_name = func[\"name\"].decode(\"ascii\")\r\n \r\n print(f\"found {func_dll}.{func_name} at {hex(address)}\")\r\nThis was easy! right? Next, we need to integrate our scipt with Ghidra to actually use the information we got here.\r\nThis will help us to see extracted API names inside Ghidra.\r\nIntegrating Qiling with Ghidra\r\nAs you probably know Ghidra uses Jython and Jython only supports Python version 2 but Qiling is based on\r\nPython version 3. I found an interesting project called ghidra_bridge link! that helps us solve this problem.\r\nSo Ghidra Bridge is an effort to sidestep that problem - instead of being stuck in Jython, set up an RPC\r\nproxy for Python objects, so we can call into Ghidra/Jython-land to get the data we need, then bring it\r\nback to a more up-to-date Python with all the packages you need to do your work.\r\nAfter installing ghidra_bridge you can find an example inside the installation directory called\r\nexample_py3_from_ghidra_bridge.py . By opening this file we will have an idea about how to write scripts based\r\non ghidra_bridge . Let’s dissect it.\r\nMost scripts should use this minimal template:\r\ndef run_script(server_host, server_port):\r\n import ghidra_bridge\r\n with ghidra_bridge.GhidraBridge(namespace=globals(), response_timeout=500):\r\n pass\r\nif __name__ == \"__main__\":\r\n in_ghidra = False\r\n try:\r\n import ghidra\r\n # we're in ghidra!\r\n in_ghidra = True\r\n except ModuleNotFoundError:\r\n # not ghidra\r\n pass\r\n if in_ghidra:\r\nhttps://lopqto.me/posts/automated-dynamic-import-resolving\r\nPage 9 of 13\n\nimport ghidra_bridge_server\r\n script_file = getSourceFile().getAbsolutePath()\r\n # spin up a ghidra_bridge_server and spawn the script in external python to connect back to it\r\n ghidra_bridge_server.GhidraBridgeServer.run_script_across_ghidra_bridge(script_file)\r\n else:\r\n # we're being run outside ghidra! (almost certainly from spawned by run_script_across_ghidra_bridge())\r\n parser = argparse.ArgumentParser(\r\n description=\"py3 script that's expected to be called from ghidra with a bridge\")\r\n # the script needs to handle these command-line arguments and use them to connect back to the ghidra ser\r\n parser.add_argument(\"--connect_to_host\", type=str, required=False,\r\n default=\"127.0.0.1\", help=\"IP to connect to the ghidra_bridge server\")\r\n parser.add_argument(\"--connect_to_port\", type=int, required=True,\r\n help=\"Port to connect to the ghidra_bridge server\")\r\n args = parser.parse_args()\r\n run_script(server_host=args.connect_to_host,\r\n server_port=args.connect_to_port)\r\nWe only need to focus on run_script() function. The other part is static and probably there is no need to\r\nchange. Only inside run_script() you are allowed to use Python 3 syntax and only here you are allowed to load\r\nPython 3 libraries (like Qiling). As you may notice I added response_timeout to the GhidraBridge object and\r\nsets it’s value to 500 seconds. Why? because as we discussed earlier emulating is a time-consuming task and\r\nemulating decryptor functions is likely more time-consuming because there is so much instruction code that needs\r\nto be emulated. So we need to set response_timeout to prevent any timeout-related errors.\r\nLeaving aside the base template, we can now write our Qiling code inside run_script() .\r\ndef run_script(server_host, server_port):\r\n from qiling import Qiling\r\n import ghidra_bridge\r\n with ghidra_bridge.GhidraBridge(namespace=globals(), response_timeout=500):\r\n ql = Qiling([\"/home/lopqto/w/automated/samples/netwalker.exe\"], \"/home/lopqto/w/automated/rootfs/x86_win\r\n ql.hook_code(extract_eax)\r\n ql.run(begin=0x0040c1a0, end=0x0040c1a5)\r\nBack to the extract_eax() function, we need to integrate it with Ghidra and add extracted API names as a\r\ncomment into Ghidra. To add a comment from a script first of all we need an address (location). We have the\r\naddress value from Qiling but we need to convert this value to Ghidra’s Address type.\r\nTo do this we need memory.blocks object from currentProgram API. But there is a challenge here.\r\ncurrentProgram API only is accessible inside run_script() . But we need this API inside extract_eax()\r\nhttps://lopqto.me/posts/automated-dynamic-import-resolving\r\nPage 10 of 13\n\ncallback. There is a cool trick to handle this situation. You need to pass things around with ql object like below:\r\nql.target_block = currentProgram.memory.blocks[0]\r\nNow we can access to ql.target_block inside extract_eax() . target_block ( memory.blocks[0] ) points to\r\nthe PE entrypoint at 0x00400000 . to convert address to Address type we need to calculate offset and do\r\nsomething like this:\r\ntarget_address = ql.target_block.getStart()\r\ntarget_address = target_address.add(address - 0x00400000)\r\nNow we have our target_address so we need one more step. accessing comment API is similar to above. First\r\nwe need getListring() object:\r\nql.listing = currentProgram.getListing()\r\nAnd to add a comment we can do:\r\ncodeUnit = ql.listing.getCodeUnitAt(target_address)\r\ncomment_message = \"{}.{}\".format(func_dll, func_name)\r\ncodeUnit.setComment(codeUnit.PRE_COMMENT, comment_message)\r\nFull source code for extract_eax() will be this:\r\ndef extract_eax(ql, address, size):\r\n buf = ql.mem.read(address, size)\r\n if \"8941\" in buf.hex(): # dword ptr [ECX + hex],EAX\r\n \r\n eax_value = ql.reg.eax\r\n func = ql.loader.import_symbols[eax_value]\r\n func_dll = func[\"dll\"]\r\n func_name = func[\"name\"].decode(\"ascii\")\r\n target_address = ql.target_block.getStart()\r\n target_address = target_address.add(address - 0x00400000)\r\n codeUnit = ql.listing.getCodeUnitAt(target_address)\r\n comment = \"{}.{}\".format(func_dll, func_name)\r\n codeUnit.setComment(codeUnit.PRE_COMMENT, comment)\r\nNow we have a Ghidra script that will use Python3 to run samples trough Qiling and extract dynamic resolved\r\nfunction names and comment them into Ghidra. See the final result:\r\nhttps://lopqto.me/posts/automated-dynamic-import-resolving\r\nPage 11 of 13\n\nAnd we are done. :)\r\nTips and tricks\r\nTwo tricks helped me to make this script. First of all, tracing the binary and printing assembly instructions can\r\nhelp a lot while debugging source!:\r\nmd = Cs(CS_ARCH_X86, CS_MODE_64)\r\ndef print_asm(ql, address, size):\r\n buf = ql.mem.read(address, size)\r\n for i in md.disasm(buf, address):\r\n print(\":: 0x%x:\\t%s\\t%s\" %(i.address, i.mnemonic, i.op_str))\r\nql.hook_code(print_asm)\r\nYou can compare emulation result with your disassembler to debug your program.\r\nThe second tip is when you try to run a time-consuming script and write something back to Ghidra (like adding a\r\ncomment) you may face with an error like this:\r\nhttps://lopqto.me/posts/automated-dynamic-import-resolving\r\nPage 12 of 13\n\nERROR (BackgroundCommandTask) Command Failure: An unexpected error occurred while processing the command: Auto\r\nIt’s because java closed the file and to solve this problem you need to increase timeout. Open the file in\r\nghidra/support/launch.properties and add this line:\r\nVMARGS=-Dghidra.util.Swing.timeout.seconds=3600\r\nConclusion\r\nThe idea described in this article can be extended and used to analyze any other malware families that dynamically\r\nresolve imports. It’s not an ultimate general solution and you need to change things a little bit to match it against\r\nyour target binary. I tried to explain my mindset behind the scene as much as possible to help you in this process.\r\nHope this post was helpful.\r\nDon’t hesitate to ping me if there is something wrong or if you want to discuss about the post. I dropped the final\r\nscript and the malware sample here!.\r\nRead more\r\nQiling website\r\nQiling docs\r\nGhidra docs\r\nIDA Pro Scripting Intro - Automate Dynamic Import Resolving for REvil Ransomware\r\nSource: https://lopqto.me/posts/automated-dynamic-import-resolving\r\nhttps://lopqto.me/posts/automated-dynamic-import-resolving\r\nPage 13 of 13",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://lopqto.me/posts/automated-dynamic-import-resolving"
	],
	"report_names": [
		"automated-dynamic-import-resolving"
	],
	"threat_actors": [],
	"ts_created_at": 1775441436,
	"ts_updated_at": 1775791198,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/67bd2633e208bae0f4ab3e2f747adf06b82a199e.pdf",
		"text": "https://archive.orkl.eu/67bd2633e208bae0f4ab3e2f747adf06b82a199e.txt",
		"img": "https://archive.orkl.eu/67bd2633e208bae0f4ab3e2f747adf06b82a199e.jpg"
	}
}