# PhotoLoader ICEDID **[research.openanalysis.net/icedid/bokbot/photoloader/config/2023/04/06/photoloader.html](https://research.openanalysis.net/icedid/bokbot/photoloader/config/2023/04/06/photoloader.html)** OALABS Research April 6, 2023 ## Overview #### Photoloader is the initial loader stage used to load ICEDID, ICEDID was originally used for banking credential theft with a later pivot as a reconnaissance tool for pre-ransomware intrusions. The webinjects used for credential theft are still active though this malware is most often associated with ransomware incidents. According to Proofpoint there is a fork of ICEDID that does not have webinject capability and is possibly developed by three separate actors... Standard IcedID Variant – The variant most commonly observed in the threat landscape and used by a variety of threat actors. Lite IcedID Variant – New variant observed as a follow-on payload in November Emotet infections that does not exfiltrate host data in the loader checkin and a bot with minimal functionality. Forked IcedID Variant – New variant observed by Proofpoint researchers in February 2023 used by a small number of threat actors which also delivers the bot with minimal functionality. ### References #### DFIRReport:ICEDID -> Quantum ransomware- ICEDIDs network infrastructure is alive and well ICEDID Configuration Extractor Fork in the Ice: The New Era of IcedID icedid_peloader.py New version of IcedID Trojan uses steganographic payloads ### Samples #### new loader sample (dfir report)2db4fadfb2565fd9474e4d5303f953e96ac248de3267014c32e8a669e7e600e0 UnpacMe older sample ``` 963397cec08790b25ff273cbe4b133634ae045d5ff8a4492e6f585f2ad14db65UnpacMe ``` ----- #### old unpacked 32bit photoloader `1b01700425c30c2c498718966aee96cfdebacc2f6167576f7aa56e3f43ec3282` [malpedia](https://malshare.com/sample.php?action=detail&hash=1b01700425c30c2c498718966aee96cfdebacc2f6167576f7aa56e3f43ec3282) ## Analysis #### It looks like the Malpedia photoloader yara rules are a bit too loose and match the newer "gzip" variant of the loader. The config location/encryption is different between these two loaders and photoloader has not been used in a few years. We are going to create a new rule that will be used to only match the newer variants. ### Rule #### This rule is heavily influenced by the elastic rules in their config extractor ``` rule icedid_loader { strings: $a1 = "; _gat=" wide fullword $a2 = "; _ga=" wide fullword $a3 = "; _u=" wide fullword $a4 = "; __io=" wide fullword $a5 = "; _gid=" wide fullword $a6 = "loader_dll_64.dll" ascii fullword $config_decryption1 = {45 33 C0 4C 8D 0D ?? ?? ?? ?? 49 2B C9 4B 8D 14 08 49 FF C0 8A 42 ?? 32 02 88 44 11 ?? 49 83 F8 } $config_decryption2 = { 00 42 8A 44 01 ?? 42 32 04 01 88 44 0D ?? 48 FF C1 48 83 F9 } condition: filesize < 60000 and ( (3 of ($a*) and $config_decryption1) or $config_decryption2 ) } ### Config Extractor #### This is a modified version of the elastic config extractor import pefile import re import struct file_data = open('/tmp/samples/963397cec08790b25ff273cbe4b133634ae045d5ff8a4492e6f585f2ad14db65', 'rb').read() pe = pefile.PE(data = file_data) ``` ----- ``` IMAGE_SCN_CNT_CODE 0x00000020 def xor(data, key): out = [] for i in range(len(data)): out.append(data[i] ^ key[i % len(key)]) return bytes(out) def is_ascii(s): return all((c < 128 and c > 39) or c == 0 for c in s) key = None domain = None campaign_id = None mapped = False if pe.sections[0].get_data()[:100] == b'\x00'*100: print("Mapped!") mapped = True for s in pe.sections: if (s.Characteristics & IMAGE_SCN_CNT_CODE) == 0: if mapped: section_data = file_data[s.VirtualAddress:s.VirtualAddress +256] else: section_data = s.get_data() if len(section_data) < 250: print("Section too small") continue # This is a hack to skip stuff that doesn't look like a key tmp_key = section_data[:32] if b'\x00'*10 in tmp_key: print("Too many nulls in key") continue data = section_data[64:96] tmp_config = xor(data, tmp_key) domain = None try: domains = tmp_config[4:] domains = domains.split(b"\x00") if not is_ascii(domains[0]): continue domain = domains[0].decode("UTF-8") except: print("Domain decode error") continue if len(domain) < 5: print("Domain too small") continue # If we are here we have a config! ``` ----- ``` campaign_id struct.unpack( 39) or c == 0 for c in s) def extract_config(file_path): file_data = open(file_path, 'rb').read() pe = pefile.PE(data = file_data) mapped = False if pe.sections[0].get_data()[:100] == b'\x00'*100: #print("Mapped!") mapped = True key = None domain = None campaign_id = None try: for s in pe.sections: if (s.Characteristics & IMAGE_SCN_CNT_CODE) == 0: if mapped: section_data = file_data[s.VirtualAddress:s.VirtualAddress +256] else: section_data = s.get_data() if len(section_data) < 250: #print("Section too small") continue # This is a hack to skip stuff that doesn't look like a key tmp_key = section_data[:32] if b'\x00'*10 in tmp_key: #print("Too many nulls in key") continue data = section_data[64:96] tmp_config = xor(data, tmp_key) domain = None try: domains = tmp_config[4:] domains = domains.split(b"\x00") if not is_ascii(domains[0]): continue domain = domains[0].decode("UTF-8") except: #print("Domain decode error") continue if len(domain) < 5: #print("Domain too small") continue # If we are here we have a config! campaign_id = struct.unpack('