{
	"id": "138979e0-2eae-4642-9cfc-2e78dd945183",
	"created_at": "2026-04-06T00:12:23.364074Z",
	"updated_at": "2026-04-10T03:19:56.112862Z",
	"deleted_at": null,
	"sha1_hash": "90461a5f6fe46c819f7f3fb0e9240c6e3dcaf76f",
	"title": "SmokeLoader Triage",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 59898,
	"plain_text": "SmokeLoader Triage\r\nPublished: 2022-08-25 · Archived: 2026-04-05 19:07:51 UTC\r\nStage 2\r\nOpaque predicate deobfuscation\r\nFrom this blog we have a simple jmp fix script.\r\nimport idc\r\nea = 0\r\nwhile True:\r\n ea = min(idc.find_binary(ea, idc.SEARCH_NEXT | idc.SEARCH_DOWN, \"74 ? 75 ?\"), # JZ / JNZ\r\n idc.find_binary(ea, idc.SEARCH_NEXT | idc.SEARCH_DOWN, \"75 ? 74 ?\")) # JNZ / JZ\r\n if ea == idc.BADADDR:\r\n break\r\n idc.patch_byte(ea, 0xEB) # JMP\r\n idc.patch_byte(ea+2, 0x90) # NOP\r\n idc.patch_byte(ea+3, 0x90) # NOP\r\n``\r\nOnce we fix the jmps we need to nop out the junk code between the code to allow IDA to convert this i\r\n```python\r\nimport idaapi\r\nstart = 0x00402DDD\r\nend = 0x00402EBF\r\nptr = start\r\nwhile ptr \u003c= end:\r\n next_ptr = next_head(ptr)\r\n junk_bytes = next_ptr - ptr\r\n if ida_bytes.get_bytes(ptr, 1) == b'\\xeb':\r\n idaapi.patch_bytes(ptr, junk_bytes * b'\\x90')\r\n ptr = next_ptr\r\nOr, we could use this excellent script from @anthonyprintup\r\nimport ida_ua\r\nimport ida_name\r\nimport ida_bytes\r\nhttps://research.openanalysis.net/smoke/smokeloader/loader/config/yara/triage/2022/08/25/smokeloader.html\r\nPage 1 of 5\n\ndef decode_instruction(ea: int) -\u003e ida_ua.insn_t:\r\n instruction: ida_ua.insn_t = ida_ua.insn_t()\r\n instruction_length = ida_ua.decode_insn(instruction, ea)\r\n if not instruction_length:\r\n return None\r\n return instruction\r\ndef main():\r\n begin: int = ida_name.get_name_ea(idaapi.BADADDR, \"start\")\r\n end: int = begin + 0xE2\r\n instructions: dict[int, ida_ua.insn_t] = {}\r\n # Undefine the current code\r\n ida_bytes.del_items(begin, 0, end)\r\n # Follow the control flow and create instructions\r\n instruction_ea: int = begin\r\n while instruction_ea \u003c= end:\r\n if instruction_ea not in instructions.keys():\r\n instruction: ida_ua.insn_t = ida_ua.insn_t()\r\n instruction_length: int = ida_ua.create_insn(instruction_ea, instruction)\r\n else:\r\n instruction: ida_ua.insn_t = decode_instruction(instruction_ea)\r\n instruction_length: int = instruction.size\r\n if not instruction_length:\r\n print(f\"Failed to create an instruction at address {instruction_ea=:#x}\")\r\n return\r\n # Append the current instruction address to the list\r\n instructions[instruction.ip] = instruction\r\n # Handle unconditional jumps\r\n current_instruction_mnemonic: str = instruction.get_canon_mnem()\r\n next_instruction: ida_ua.insn_t | None = decode_instruction(instruction_ea + instruction.size\r\n if next_instruction is not None:\r\n next_instruction_mnemonic: str = next_instruction.get_canon_mnem()\r\n if (current_instruction_mnemonic == \"jnz\" and next_instruction_mnemonic == \"jz\") or \\\r\n (current_instruction_mnemonic == \"jz\" and next_instruction_mnemonic == \"jnz\"):\r\n # Unconditional jump detected\r\n assert instruction.ops[0].type == ida_ua.o_near\r\n instruction_ea = instruction.ops[0].addr\r\n ida_ua.create_insn(next_instruction.ip)\r\n instructions[next_instruction.ip] = next_instruction\r\nhttps://research.openanalysis.net/smoke/smokeloader/loader/config/yara/triage/2022/08/25/smokeloader.html\r\nPage 2 of 5\n\ncontinue\r\n if current_instruction_mnemonic == \"jmp\":\r\n assert instruction.ops[0].type == ida_ua.o_near\r\n instruction_ea = instruction.ops[0].addr\r\n else:\r\n instruction_ea += instruction.size\r\n # NOP the remaining instructions\r\n for ea in range(begin, end):\r\n skip: bool = False\r\n for _, instruction in instructions.items():\r\n if ea in range(instruction.ip, instruction.ip + instruction.size):\r\n skip = True\r\n break\r\n if skip:\r\n continue\r\n # Patch the address\r\n ida_bytes.patch_bytes(ea, b\"\\x90\")\r\nif __name__ == \"__main__\":\r\n main()\r\nAfter this we can see that the next function address is built using some stack/ret manipulation.\r\nGeneric Opaque Predicate Patching\r\nThere is also this nice generic patching script from Alex: nopme.py.\r\nFunction Decryption\r\nSome functions are encrypted. We can find the first one by following the obfuscated control flow until the first\r\ncall . This call calls into a function which then calls the decryption function. The decryption function takes a\r\nsize and a offset to the function that needs to be decrypted. The size is placed in the ecx register, and the\r\nfunction offset follows the call.\r\nThe decryption itself is a single byte xor but the decryption key is moved into the edx register as a full DWORD\r\n(we only used the LSB).\r\nFrom this blog we have a simple deobfuscation script updated for our sample. This script didn't perform well for\r\nsome reason so we ended up manually decrypting the functions!\r\nimport idc\r\nimport idautils\r\nhttps://research.openanalysis.net/smoke/smokeloader/loader/config/yara/triage/2022/08/25/smokeloader.html\r\nPage 3 of 5\n\ndef xor_chunk(offset, n):\r\n ea = 0x400000 + offset\r\n for i in range(n):\r\n byte = ord(idc.get_bytes(ea+i, 1))\r\n byte ^= 0x50\r\n idc.patch_byte(ea+i, byte)\r\ndef decrypt(xref):\r\n call_xref = list(idautils.CodeRefsTo(xref, 0))[0]\r\n while True:\r\n if idc.print_insn_mnem(call_xref) == 'push' and idc.get_operand_type(call_xref, 0) == idaapi\r\n n = idc.get_operand_value(call_xref, 0)\r\n break\r\n if idc.print_insn_mnem(call_xref) == 'mov' and idc.get_operand_type(call_xref, 1) == idaapi.o\r\n n = idc.get_operand_value(call_xref, 1)\r\n break\r\n call_xref = prev_head(call_xref)\r\n n = idc.get_operand_value(call_xref, 0)\r\n offset = (xref + 5) - 0x400000\r\n xor_chunk(offset, n)\r\n idc.create_insn(offset+0x400000)\r\n ida_funcs.add_func(offset+0x400000)\r\nxor_chunk_addr = 0x00401118 # address of the xoring function\r\ndecrypt_xref_list = idautils.CodeRefsTo(xor_chunk_addr, 0)\r\nfor xref in decrypt_xref_list:\r\n decrypt(xref)\r\nAPI Hashing\r\nAccording to this blog we are expecting to see some API hashing using the djb2 algorithm. We can try to find this\r\nfunction by searching for the constant 0x1505.\r\nThough the djb2 algorithm is used for the API hashing the malware also encrypts the hashes with a hard coded\r\nXOR key. In our sample the key is 0x76186250.\r\nDecryption\r\nThere is a 32-bit and a 64-bit version of stage 3 stored consecutivly in the binary. The data is encrypted with a\r\nhard coded 4-byte XOR key, the decryption must be a multiple of four. The trailing bytes (if any) are then\r\ndecrypted with a single byte XOR. In our sample the DWORD key is 0x76186250 and the single byte key is 0x50.\r\nhttps://research.openanalysis.net/smoke/smokeloader/loader/config/yara/triage/2022/08/25/smokeloader.html\r\nPage 4 of 5\n\nDecompression\r\nOnce the stage 3 data is decrypted it is also decompressed with the LZSA2 algorithm. We matched this with a\r\nblog. The LZSA algorithm is detailed on this github Emmanuel Marty/LZSA.\r\nSource: https://research.openanalysis.net/smoke/smokeloader/loader/config/yara/triage/2022/08/25/smokeloader.html\r\nhttps://research.openanalysis.net/smoke/smokeloader/loader/config/yara/triage/2022/08/25/smokeloader.html\r\nPage 5 of 5",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://research.openanalysis.net/smoke/smokeloader/loader/config/yara/triage/2022/08/25/smokeloader.html"
	],
	"report_names": [
		"smokeloader.html"
	],
	"threat_actors": [],
	"ts_created_at": 1775434343,
	"ts_updated_at": 1775791196,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/90461a5f6fe46c819f7f3fb0e9240c6e3dcaf76f.pdf",
		"text": "https://archive.orkl.eu/90461a5f6fe46c819f7f3fb0e9240c6e3dcaf76f.txt",
		"img": "https://archive.orkl.eu/90461a5f6fe46c819f7f3fb0e9240c6e3dcaf76f.jpg"
	}
}