├── utils ├── __init__.py ├── noderatscan.py ├── azorultscan.py ├── njratscan.py ├── elf_wellmess.py ├── poisonivyscan.py ├── emotetscan.py ├── lokibotscan.py ├── wellmessscan.py ├── trickbotscan.py ├── agentteslascan.py ├── elf_pleadscan.py ├── smokeloaderscan.py ├── hawkeyescan.py ├── xxmmscan.py ├── netwirescan.py ├── nanocorescan.py ├── beblohscan.py ├── ramnitscan.py ├── asyncratscan.py ├── quasarscan.py ├── remcosscan.py ├── cobaltstrikescan.py ├── formbookscan.py ├── redleavesscan.py ├── tscookiescan.py ├── formbook_decryption.py ├── datperscan.py └── aplib.py ├── .gitignore ├── .github └── workflows │ └── .yara-ci.yml ├── images ├── sample1.png ├── sample2.png ├── sample3.png ├── sample4.png └── logo.svg ├── requirements.txt ├── LICENSE.txt └── README.md /utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *~ 3 | *.bak 4 | -------------------------------------------------------------------------------- /.github/workflows/.yara-ci.yml: -------------------------------------------------------------------------------- 1 | files: 2 | accept: 3 | - "yara/rule.yara" 4 | -------------------------------------------------------------------------------- /images/sample1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JPCERTCC/MalConfScan/HEAD/images/sample1.png -------------------------------------------------------------------------------- /images/sample2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JPCERTCC/MalConfScan/HEAD/images/sample2.png -------------------------------------------------------------------------------- /images/sample3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JPCERTCC/MalConfScan/HEAD/images/sample3.png -------------------------------------------------------------------------------- /images/sample4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JPCERTCC/MalConfScan/HEAD/images/sample4.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | distorm3 2 | pycrypto 3 | yara-python 4 | pefile 5 | pbkdf2 6 | distorm3 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The 3-Clause BSD License 2 | 3 | SPDX short identifier: BSD-3-Clause 4 | Note: This license has also been called the "New BSD License" or "Modified BSD License". See also the 2-clause BSD License. 5 | 6 | --- 7 | 8 | Copyright 2023 JPCERT Coordination Center 9 | 10 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 11 | 12 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 13 | 14 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 15 | 16 | 3. Neither JPCERT Coordination Center nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 19 | 20 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | [![Arsenal](https://rawgit.com/toolswatch/badges/master/arsenal/usa/2019.svg)](http://www.toolswatch.org/2019/05/amazing-black-hat-arsenal-usa-2019-lineup-announced/) 4 | 5 | ## Concept 6 | **MalConfScan** is a [Volatility](https://github.com/volatilityfoundation/volatility) plugin extracts configuration data of known malware. Volatility is an open-source memory forensics framework for incident response and malware analysis. This tool searches for malware in memory images and dumps configuration data. In addition, this tool has a function to list strings to which malicious code refers. 7 | 8 | ![MalConfScan sample](images/sample1.png) 9 | 10 | ## Supported Malware Families 11 | MalConfScan can dump the following malware configuration data, decoded strings or DGA domains: 12 | 13 | - [x] Ursnif 14 | - [x] Emotet 15 | - [x] Smoke Loader 16 | - [x] PoisonIvy 17 | - [x] CobaltStrike 18 | - [x] NetWire 19 | - [x] PlugX 20 | - [x] RedLeaves / Himawari / Lavender / Armadill / zark20rk 21 | - [x] TSCookie 22 | - [x] TSC_Loader 23 | - [x] xxmm 24 | - [x] Datper 25 | - [x] Ramnit 26 | - [x] HawkEye 27 | - [x] Lokibot 28 | - [x] Bebloh (Shiotob/URLZone) 29 | - [x] AZORult 30 | - [x] NanoCore RAT 31 | - [x] AgentTesla 32 | - [x] FormBook 33 | - [x] NodeRAT (https://blogs.jpcert.or.jp/ja/2019/02/tick-activity.html) 34 | - [x] njRAT 35 | - [x] TrickBot 36 | - [x] Remcos 37 | - [x] QuasarRAT 38 | - [x] AsyncRAT 39 | - [x] WellMess (Windows/Linux) 40 | - [x] ELF_PLEAD 41 | - [ ] Pony 42 | 43 | ## Additional Analysis 44 | MalConfScan has a function to list strings to which malicious code refers. Configuration data is usually encoded by malware. Malware writes decoded configuration data to memory, it may be in memory. This feature may list decoded configuration data. 45 | 46 | ## How to Install 47 | If you want to know more details, please check [the MalConfScan wiki](https://github.com/JPCERTCC/MalConfScan/wiki). 48 | 49 | ## How to Use 50 | MalConfScan has two functions **malconfscan**, **linux_malconfscan** and **malstrscan**. 51 | 52 | ### Export known malware configuration 53 | ``` 54 | $ python vol.py malconfscan -f images.mem --profile=Win7SP1x64 55 | ``` 56 | 57 | ### Export known malware configuration for Linux 58 | ``` 59 | $ python vol.py linux_malconfscan -f images.mem --profile=LinuxDebianx64 60 | ``` 61 | 62 | ### List the referenced strings 63 | ``` 64 | $ python vol.py malstrscan -f images.mem --profile=Win7SP1x64 65 | ``` 66 | 67 | ## Overview & Demonstration 68 | 69 | Following [YouTube video](https://youtu.be/n36WAzgHldY) shows the overview of MalConfScan. 70 | 71 | [![MalConfScan_Overview](https://img.youtube.com/vi/n36WAzgHldY/sddefault.jpg)](https://youtu.be/n36WAzgHldY) 72 | 73 | And, following [YouTube video](https://youtu.be/kPsOvoRHK3k) is the demonstration of MalConfScan. 74 | 75 | [![MalConfScan_Demonstration](https://img.youtube.com/vi/kPsOvoRHK3k/sddefault.jpg)](https://youtu.be/kPsOvoRHK3k) 76 | 77 | ## MalConfScan with Cuckoo 78 | Malware configuration data can be dumped automatically by adding MalConfScan to Cuckoo Sandbox. If you need more details on Cuckoo and MalConfScan integration, please check [MalConfScan with Cuckoo](https://github.com/JPCERTCC/MalConfScan-with-Cuckoo). 79 | -------------------------------------------------------------------------------- /images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 17 | 18 | 20 | image/svg+xml 21 | 23 | 24 | 25 | 26 | 27 | 30 | 34 | 42 | 45 | 50 | 54 | 55 | 59 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /utils/noderatscan.py: -------------------------------------------------------------------------------- 1 | # Detecting NodeRat for Volatilitv 2 | # 3 | # LICENSE 4 | # Please refer to the LICENSE.txt in the https://github.com/JPCERTCC/MalConfScan/ 5 | # 6 | # How to use: 7 | # 1. cd "Volatility Folder" 8 | # 2. mv noderatconfigallocate.py volatility/plugins/malware 9 | # 3. python vol.py noderatconfig -f images.mem --profile=Win7SP1x64 10 | 11 | import volatility.plugins.taskmods as taskmods 12 | import volatility.win32.tasks as tasks 13 | import volatility.utils as utils 14 | import volatility.debug as debug 15 | import volatility.plugins.malware.malfind as malfind 16 | import re 17 | import json 18 | from struct import unpack, unpack_from 19 | 20 | try: 21 | import yara 22 | has_yara = True 23 | except ImportError: 24 | has_yara = False 25 | 26 | noderat_sig = { 27 | 'namespace1' : 'rule Noderat { \ 28 | strings: \ 29 | $config = "/config/app.json" \ 30 | $key = "/config/.regeditKey.rc" \ 31 | $message = "uninstall error when readFileSync: " \ 32 | condition: all of them}' 33 | } 34 | 35 | # Config pattern 36 | CONFIG_PATTERNS = [re.compile("\x7B\x0D\x0A\x20\x20\x22\x6E\x61\x6D\x65\x22\x3A\x20(.*)\x65\x0d\x0a\x7d", re.DOTALL)] 37 | 38 | 39 | class noderatConfig(taskmods.DllList): 40 | "Parse the Noderat configuration" 41 | 42 | @staticmethod 43 | def is_valid_profile(profile): 44 | return (profile.metadata.get('os', 'unknown') == 'windows'), profile.metadata.get('memory_model', '32bit') 45 | 46 | def get_vad_base(self, task, address): 47 | for vad in task.VadRoot.traverse(): 48 | if address >= vad.Start and address < vad.End: 49 | return vad.Start, vad.End 50 | return None 51 | 52 | def calculate(self): 53 | 54 | if not has_yara: 55 | debug.error('Yara must be installed for this plugin.') 56 | 57 | addr_space = utils.load_as(self._config) 58 | 59 | os, memory_model = self.is_valid_profile(addr_space.profile) 60 | if not os: 61 | debug.error('This command does not support the selected profile.') 62 | 63 | rules = yara.compile(sources=noderat_sig) 64 | 65 | for task in self.filter_tasks(tasks.pslist(addr_space)): 66 | scanner = malfind.VadYaraScanner(task=task, rules=rules) 67 | for hit, address in scanner.scan(): 68 | 69 | vad_base_addr, end = self.get_vad_base(task, address) 70 | proc_addr_space = task.get_process_address_space() 71 | memdata = proc_addr_space.get_available_addresses() 72 | 73 | config_data = [] 74 | 75 | for m in memdata: 76 | if m[1] < 0x100000: 77 | continue 78 | p_data = {} 79 | 80 | data = proc_addr_space.zread(m[0], m[1]) 81 | 82 | for pattern in CONFIG_PATTERNS: 83 | m = re.search(pattern, data) 84 | 85 | if m: 86 | offset = m.start() 87 | else: 88 | continue 89 | 90 | json_data = data[offset:m.end()] 91 | d = json.loads(json_data) 92 | 93 | config_data.append(d) 94 | break 95 | yield task, vad_base_addr, end, hit, memory_model, config_data 96 | break 97 | 98 | def render_text(self, outfd, data): 99 | 100 | delim = '-' * 70 101 | 102 | for task, start, end, malname, memory_model, config_data in data: 103 | outfd.write("{0}\n".format(delim)) 104 | outfd.write("Process: {0} ({1})\n\n".format(task.ImageFileName, task.UniqueProcessId)) 105 | 106 | outfd.write("[Config Info]\n") 107 | for p_data in config_data: 108 | for id, param in p_data.items(): 109 | outfd.write("{0:<10}: {1}\n".format(id, param)) 110 | -------------------------------------------------------------------------------- /utils/azorultscan.py: -------------------------------------------------------------------------------- 1 | # Detecting Azorult for Volatilitv 2 | # 3 | # LICENSE 4 | # Please refer to the LICENSE.txt in the https://github.com/JPCERTCC/MalConfScan/ 5 | # 6 | # How to use: 7 | # 1. cd "Volatility Folder" 8 | # 2. mv azorultconfigallocate.py volatility/plugins/malware 9 | # 3. python vol.py azorultconfig -f images.mem --profile=Win7SP1x64 10 | 11 | import volatility.plugins.taskmods as taskmods 12 | import volatility.win32.tasks as tasks 13 | import volatility.utils as utils 14 | import volatility.debug as debug 15 | import volatility.plugins.malware.malfind as malfind 16 | import re 17 | from struct import unpack, unpack_from 18 | 19 | try: 20 | import yara 21 | has_yara = True 22 | except ImportError: 23 | has_yara = False 24 | 25 | azorult_sig = { 26 | 'namespace1' : 'rule Azorult { \ 27 | strings: \ 28 | $v1 = "Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 5.1)" \ 29 | $v2 = "http://ip-api.com/json" \ 30 | $v3 = { c6 07 1e c6 47 01 15 c6 47 02 34 } \ 31 | condition: all of them}' 32 | } 33 | 34 | # Config pattern 35 | CONFIG_PATTERNS = [re.compile("[-+]{10}\x0D\x0A", re.DOTALL)] 36 | 37 | 38 | class azorultConfig(taskmods.DllList): 39 | "Parse the Azorult configuration" 40 | 41 | @staticmethod 42 | def is_valid_profile(profile): 43 | return (profile.metadata.get('os', 'unknown') == 'windows'), profile.metadata.get('memory_model', '32bit') 44 | 45 | def get_vad_base(self, task, address): 46 | for vad in task.VadRoot.traverse(): 47 | if address >= vad.Start and address < vad.End: 48 | return vad.Start, vad.End 49 | return None 50 | 51 | def calculate(self): 52 | 53 | if not has_yara: 54 | debug.error('Yara must be installed for this plugin.') 55 | 56 | addr_space = utils.load_as(self._config) 57 | 58 | os, memory_model = self.is_valid_profile(addr_space.profile) 59 | if not os: 60 | debug.error('This command does not support the selected profile.') 61 | 62 | rules = yara.compile(sources=azorult_sig) 63 | 64 | for task in self.filter_tasks(tasks.pslist(addr_space)): 65 | scanner = malfind.VadYaraScanner(task=task, rules=rules) 66 | for hit, address in scanner.scan(): 67 | 68 | vad_base_addr, end = self.get_vad_base(task, address) 69 | proc_addr_space = task.get_process_address_space() 70 | memdata = proc_addr_space.get_available_addresses() 71 | 72 | config_data = [] 73 | 74 | for m in memdata: 75 | if m[1] < 0x100000: 76 | continue 77 | p_data = {} 78 | 79 | data = proc_addr_space.zread(m[0], m[1]) 80 | 81 | for pattern in CONFIG_PATTERNS: 82 | m = re.search(pattern, data) 83 | 84 | if m: 85 | offset = m.start() - 0x1c 86 | else: 87 | continue 88 | 89 | i = 0 90 | while(True): 91 | _, _, param_len = unpack_from(" 0x100: 93 | break 94 | offset = offset + 0xc 95 | param_data = data[offset:offset + param_len] 96 | p_data[i] = param_data 97 | rest_len = 4 - (param_len % 4) 98 | offset += param_len + rest_len 99 | i += 1 100 | 101 | config_data.append(p_data) 102 | yield task, vad_base_addr, end, hit, memory_model, config_data 103 | break 104 | 105 | def render_text(self, outfd, data): 106 | 107 | delim = '-' * 70 108 | 109 | for task, start, end, malname, memory_model, config_data in data: 110 | outfd.write("{0}\n".format(delim)) 111 | outfd.write("Process: {0} ({1})\n\n".format(task.ImageFileName, task.UniqueProcessId)) 112 | 113 | outfd.write("[Download Config Info]\n") 114 | for p_data in config_data: 115 | for id, param in p_data.items(): 116 | outfd.write("{0:<4}: {1}\n".format(id, param)) 117 | -------------------------------------------------------------------------------- /utils/njratscan.py: -------------------------------------------------------------------------------- 1 | # Detecting njRAT Keylogger for Volatility 2 | # 3 | # LICENSE 4 | # Please refer to the LICENSE.txt in the https://github.com/JPCERTCC/MalConfScan/ 5 | # 6 | # How to use: 7 | # 1. cd "Volatility Folder" 8 | # 2. mv njratscan.py volatility/plugins/malware 9 | # 3. python vol.py njratconfig -f images.mem --profile=Win7SP1x64 10 | 11 | import volatility.plugins.taskmods as taskmods 12 | import volatility.win32.tasks as tasks 13 | import volatility.utils as utils 14 | import volatility.debug as debug 15 | import volatility.plugins.malware.malfind as malfind 16 | import re 17 | from base64 import b64decode 18 | from collections import OrderedDict 19 | 20 | try: 21 | import yara 22 | has_yara = True 23 | except ImportError: 24 | has_yara = False 25 | 26 | njrat_sig = { 27 | 'namespace1' : 'rule Njrat { \ 28 | strings: \ 29 | $reg = "SEE_MASK_NOZONECHECKS" wide \ 30 | $msg = "Execute ERROR" wide \ 31 | $ping = "cmd.exe /c ping 0 -n 2 & del" wide \ 32 | condition: all of them}' 33 | } 34 | 35 | # Config pattern 36 | CONFIG_PATTERNS = [re.compile("\x46\x69\x78\x00\x6b\x00\x57\x52\x4B\x00\x6D\x61\x69\x6E\x00\x00\x00", re.DOTALL)] 37 | 38 | idx_list = { 39 | 0: "ID", 40 | 1: "Version", 41 | 2: "Name of Executable", 42 | 3: "Copy Direcroty", 43 | 4: "Registry Name", 44 | 5: "Server", 45 | 6: "Port", 46 | 7: "Split", 47 | 8: "Registry Key", 48 | } 49 | 50 | 51 | class njratConfig(taskmods.DllList): 52 | """Parse the njRAT configuration""" 53 | 54 | @staticmethod 55 | def is_valid_profile(profile): 56 | return (profile.metadata.get('os', 'unknown') == 'windows'), profile.metadata.get('memory_model', '32bit') 57 | 58 | def get_vad_base(self, task, address): 59 | for vad in task.VadRoot.traverse(): 60 | if address >= vad.Start and address < vad.End: 61 | return vad.Start, vad.End 62 | 63 | return None 64 | 65 | def parse_config(self, configs): 66 | i = 0 67 | p_data = OrderedDict() 68 | for config in configs: 69 | if i == 0: 70 | p_data[idx_list[i]] = b64decode(config) 71 | else: 72 | p_data[idx_list[i]] = config 73 | i += 1 74 | 75 | return p_data 76 | 77 | def calculate(self): 78 | 79 | if not has_yara: 80 | debug.error("Yara must be installed for this plugin") 81 | 82 | addr_space = utils.load_as(self._config) 83 | 84 | os, memory_model = self.is_valid_profile(addr_space.profile) 85 | if not os: 86 | debug.error("This command does not support the selected profile.") 87 | 88 | rules = yara.compile(sources=njrat_sig) 89 | 90 | for task in self.filter_tasks(tasks.pslist(addr_space)): 91 | scanner = malfind.VadYaraScanner(task=task, rules=rules) 92 | 93 | for hit, address in scanner.scan(): 94 | 95 | vad_base_addr, end = self.get_vad_base(task, address) 96 | proc_addr_space = task.get_process_address_space() 97 | data = proc_addr_space.zread(vad_base_addr, end - vad_base_addr) 98 | 99 | config_data = [] 100 | 101 | offset = 0 102 | for pattern in CONFIG_PATTERNS: 103 | mc = re.search(pattern, data) 104 | if mc: 105 | offset = mc.end() 106 | 107 | configs = [] 108 | if offset > 0: 109 | while 1: 110 | strings = [] 111 | while data[offset] == "\x01" or data[offset] == "\x00": 112 | offset += 1 113 | string_len = ord(data[offset]) 114 | offset += 1 115 | for i in range(string_len): 116 | if data[offset + i] != "\x00": 117 | strings.append(data[offset + i]) 118 | if "False" not in "".join(strings) and "True" not in "".join(strings): 119 | configs.append("".join(strings)) 120 | offset = offset + string_len 121 | if len(configs) > 8: 122 | break 123 | 124 | config_data.append(self.parse_config(configs)) 125 | 126 | yield task, vad_base_addr, end, hit, memory_model, config_data 127 | break 128 | 129 | def render_text(self, outfd, data): 130 | 131 | delim = '-' * 70 132 | 133 | for task, start, end, malname, memory_model, config_data in data: 134 | outfd.write("{0}\n".format(delim)) 135 | outfd.write("Process: {0} ({1})\n\n".format(task.ImageFileName, task.UniqueProcessId)) 136 | 137 | outfd.write("[Config Info]\n") 138 | for p_data in config_data: 139 | for id, param in p_data.items(): 140 | outfd.write("{0:<21}: {1}\n".format(id, param)) 141 | -------------------------------------------------------------------------------- /utils/elf_wellmess.py: -------------------------------------------------------------------------------- 1 | # Detecting ELF_Wellmess for Volatilitv 2 | # 3 | # LICENSE 4 | # Please refer to the LICENSE.txt in the https://github.com/JPCERTCC/MalConfScan/ 5 | # 6 | # How to use: 7 | # 1. cd "Volatility Folder" 8 | # 2. mv elf_wellmessconfig.py volatility/plugins/malware 9 | # 3. python vol.py elf_wellmessconfig -f images.mem --profile=Win7SP1x64 10 | 11 | import volatility.plugins.taskmods as taskmods 12 | import volatility.win32.tasks as tasks 13 | import volatility.utils as utils 14 | import volatility.debug as debug 15 | import volatility.plugins.malware.malfind as malfind 16 | import volatility.plugins.linux.pslist as linux_pslist 17 | import volatility.plugins.linux.linux_yarascan as linux_yarascan 18 | import re 19 | import io 20 | from struct import unpack, unpack_from 21 | from collections import OrderedDict 22 | 23 | try: 24 | import yara 25 | has_yara = True 26 | except ImportError: 27 | has_yara = False 28 | 29 | elf_wellmess_sig = { 30 | 'namespace1' : 'rule elf_wellmess { \ 31 | strings: \ 32 | $botlib1 = "botlib.wellMess" ascii\ 33 | $botlib2 = "botlib.Command" ascii\ 34 | $botlib3 = "botlib.Download" ascii\ 35 | $botlib4 = "botlib.AES_Encrypt" ascii\ 36 | condition: (uint32(0) == 0x464C457F) and all of ($botlib*)}' 37 | } 38 | 39 | # Config pattern 40 | CONFIG_PATTERNS = [re.compile("\x00(.)\x00\x00\x00\x8B\x05...\x00\x85\xC0\x0F\x85..\x00\x00\x8D\x05(....)\x89\x05...\x00\xC7\x05", re.DOTALL), 41 | re.compile("\x00(.)\x00\x00\x00\x8B\x05...\x00\x85\xC0\x0F\x85..\x00\x00\x48\x8D\x05(....)\x48\x89\x05...\x00\x48\xC7\x05", re.DOTALL)] 42 | 43 | class elf_wellmessConfig(linux_pslist.linux_pslist): 44 | "Parse the ELF_Wellmess configuration" 45 | 46 | @staticmethod 47 | def is_valid_profile(profile): 48 | return profile.metadata.get('os', 'unknown'), profile.metadata.get('memory_model', '32bit') 49 | 50 | def get_vma_base(self, task, address): 51 | for vma in task.get_proc_maps(): 52 | if address >= vma.vm_start and address < vma.vm_end: 53 | return vma.vm_start, vma.vm_end 54 | 55 | return None 56 | 57 | def filter_tasks(self): 58 | tasks = linux_pslist.linux_pslist(self._config).calculate() 59 | 60 | if self._config.PID is not None: 61 | try: 62 | pidlist = [int(p) for p in self._config.PID.split(',')] 63 | except ValueError: 64 | debug.error("Invalid PID {0}".format(self._config.PID)) 65 | 66 | pids = [t for t in tasks if t.pid in pidlist] 67 | if len(pids) == 0: 68 | debug.error("Cannot find PID {0}. If its terminated or unlinked, use psscan and then supply --offset=OFFSET".format(self._config.PID)) 69 | return pids 70 | 71 | return tasks 72 | 73 | def parse_config(self, config): 74 | p_data = OrderedDict() 75 | for i, d in enumerate(config): 76 | p_data["conf " + str(i)] = d 77 | 78 | return p_data 79 | 80 | def calculate(self): 81 | 82 | if not has_yara: 83 | debug.error('Yara must be installed for this plugin.') 84 | 85 | addr_space = utils.load_as(self._config) 86 | 87 | os, memory_model = self.is_valid_profile(addr_space.profile) 88 | if not os: 89 | debug.error('This command does not support the selected profile.') 90 | 91 | rules = yara.compile(sources=elf_wellmess_sig) 92 | 93 | for task in self.filter_tasks(): 94 | scanner = linux_yarascan.VmaYaraScanner(task = task, rules = rules) 95 | for hit, address in scanner.scan(): 96 | 97 | start, end = self.get_vma_base(task, address) 98 | data = scanner.address_space.zread(start, (end - start) * 2) 99 | #data = scanner.address_space.zread(address - self._config.REVERSE, self._config.SIZE) 100 | 101 | config_data = [] 102 | configs = [] 103 | for pattern in CONFIG_PATTERNS: 104 | mc = list(re.finditer(pattern, data)) 105 | if mc: 106 | for m in mc: 107 | hit_adderss = m.span() 108 | config_rva = unpack("=I", m.groups()[1])[0] 109 | 110 | if ord(data[0x4]) == 0x2: # for 64bit 111 | config_offset = config_rva + hit_adderss[0] + 26 112 | else: # for 32bit 113 | config_offset = config_rva - 0x40000 114 | 115 | configs.append(data[config_offset:config_offset + ord(m.groups()[0])]) 116 | 117 | yield task, start, end, hit, memory_model, config_data 118 | break 119 | 120 | def render_text(self, outfd, data): 121 | 122 | delim = '-' * 70 123 | 124 | for task, start, end, malname, memory_model, config_data in data: 125 | outfd.write("{0}\n".format(delim)) 126 | outfd.write("Process: {0} ({1})\n\n".format(task.comm, task.pid)) 127 | 128 | outfd.write("[Config Info]\n") 129 | for p_data in config_data: 130 | for id, param in p_data.items(): 131 | outfd.write("{0:<20}: {1}\n".format(id, param)) 132 | -------------------------------------------------------------------------------- /utils/poisonivyscan.py: -------------------------------------------------------------------------------- 1 | # Detecting PoisonIvy for Volatility 2 | # 3 | # LICENSE 4 | # Please refer to the LICENSE.txt in the https://github.com/JPCERTCC/MalConfScan/ 5 | # 6 | # How to use: 7 | # 1. cd "Volatility Folder" 8 | # 2. mv poisonivyscan.py volatility/plugins/malware 9 | # 3. python vol.py poisonivyconfig -f images.mem --profile=Win7SP1x64 10 | 11 | import volatility.plugins.taskmods as taskmods 12 | import volatility.win32.tasks as tasks 13 | import volatility.utils as utils 14 | import volatility.debug as debug 15 | import volatility.plugins.malware.malfind as malfind 16 | import re 17 | import pefile 18 | from struct import unpack, unpack_from 19 | 20 | try: 21 | import yara 22 | has_yara = True 23 | except ImportError: 24 | has_yara = False 25 | 26 | poisonivy_sig = { 27 | 'namespace1' : 'rule PoisonIvy { \ 28 | strings: \ 29 | $a1 = { 0E 89 02 44 } \ 30 | $b1 = { AD D1 34 41 } \ 31 | $c1 = { 66 35 20 83 66 81 F3 B8 ED } \ 32 | condition: all of them}' 33 | } 34 | 35 | # idx list 36 | idx_list = { 37 | 0x012d: ["Install Name", 1], 38 | 0x0145: ["Password", 1], 39 | 0x0165: ["AcviteX Key", 1], 40 | 0x018c: ["C&C Servers Count", 0], 41 | 0x0190: ["Server", 1], 42 | 0x02c1: ["Proxy Servers Count", 0], 43 | 0x03f3: ["Install Path", 1], 44 | 0x03f6: ["AcviteX Flag", 2], 45 | 0x03f7: ["Installation Folder", 3], 46 | 0x03f8: ["Auto-remove Flag", 2], 47 | 0x03f9: ["Thread Persistence Flag", 2], 48 | 0x03fa: ["Keylog Flag", 2], 49 | 0x03fb: ["Mutex", 1], 50 | 0x040f: ["Active Setup Name", 1], 51 | 0x0418: ["Default Browser Path", 1], 52 | 0x0441: ["Injection Flag", 2], 53 | 0x0442: ["Injection Process", 1], 54 | 0x0456: ["Active Key", 1], 55 | 0x0af4: ["Proxy Hijack", 2], 56 | 0x0af5: ["Persistent Proxy", 2], 57 | 0x0afa: ["Campaign ID", 1], 58 | 0x0bf9: ["Group ID", 1], 59 | 0x0d08: ["Inject Default Browser", 2], 60 | 0x0d09: ["Registry Key Flag", 2], 61 | 0x0d12: ["ADS Flag", 2], 62 | 0x0e12: ["Registry Key Value", 1], 63 | 0x1201: ["Server", 1], 64 | 0xeffc: ["unknow 1", 2], 65 | 0xef8c: ["unknow 2", 2], 66 | 0xef7c: ["unknow 3", 2], 67 | } 68 | 69 | # Config pattern 70 | CONFIG_PATTERNS = [re.compile("\xFA\x0A(.)\x00", re.DOTALL)] 71 | 72 | MODE = {0: "Disable", 1: "Enable"} 73 | FOLDER = {1: "%systemroot%", 2: "%systemroot%\system32"} 74 | 75 | 76 | class poisonivyConfig(taskmods.DllList): 77 | """Parse the PoisonIvy configuration""" 78 | 79 | @staticmethod 80 | def is_valid_profile(profile): 81 | return (profile.metadata.get('os', 'unknown') == 'windows'), profile.metadata.get('memory_model', '32bit') 82 | 83 | def get_vad_base(self, task, address): 84 | for vad in task.VadRoot.traverse(): 85 | if address >= vad.Start and address < vad.End: 86 | return vad.Start, vad.End 87 | 88 | return None 89 | 90 | def parse_config(self, idx, data): 91 | p_data = {} 92 | if idx in idx_list: 93 | field, field_type = idx_list[idx] 94 | else: 95 | field = hex(idx) 96 | field_type = 0 97 | 98 | if field_type == 0: 99 | if unpack_from(" 0: 146 | enc = data[offset + 5:offset + 5 + size] 147 | config_data.append(self.parse_config(idx, enc)) 148 | offset = offset + size + 4 149 | 150 | yield task, vad_base_addr, end, hit, memory_model, config_data 151 | break 152 | 153 | def render_text(self, outfd, data): 154 | 155 | delim = '-' * 70 156 | 157 | for task, start, end, malname, memory_model, config_data in data: 158 | outfd.write("{0}\n".format(delim)) 159 | outfd.write("Process: {0} ({1})\n\n".format(task.ImageFileName, task.UniqueProcessId)) 160 | 161 | outfd.write("[Config Info]\n") 162 | for p_data in config_data: 163 | for id, param in p_data.items(): 164 | outfd.write("{0:<20}: {1}\n".format(id, param)) 165 | -------------------------------------------------------------------------------- /utils/emotetscan.py: -------------------------------------------------------------------------------- 1 | # Detecting Emotet for Volatility 2 | # 3 | # LICENSE 4 | # Please refer to the LICENSE.txt in the https://github.com/JPCERTCC/MalConfScan/ 5 | # 6 | # How to use: 7 | # 1. cd "Volatility Folder" 8 | # 2. mv emotetscan.py volatility/plugins/malware 9 | # 3. python vol.py emotetconfig -f images.mem --profile=Win7SP1x64 10 | 11 | import volatility.plugins.taskmods as taskmods 12 | import volatility.win32.tasks as tasks 13 | import volatility.utils as utils 14 | import volatility.debug as debug 15 | import volatility.plugins.malware.malfind as malfind 16 | import re 17 | from struct import unpack, pack 18 | from collections import OrderedDict 19 | from socket import inet_ntoa 20 | 21 | try: 22 | from Crypto.Util import asn1 23 | from Crypto.PublicKey import RSA 24 | has_crypto = True 25 | except ImportError: 26 | has_crypto = False 27 | 28 | try: 29 | import yara 30 | has_yara = True 31 | except ImportError: 32 | has_yara = False 33 | 34 | emotet_sig = { 35 | 'namespace1' : 'rule Emotet { \ 36 | strings: \ 37 | $v4a = { BB 00 C3 4C 84 } \ 38 | $v4b = { B8 00 C3 CC 84 } \ 39 | $v5a = { 6D 4E C6 41 33 D2 81 C1 39 30 00 00 } \ 40 | $v6a = { C7 40 20 ?? ?? ?? 00 C7 40 10 ?? ?? ?? 00 C7 40 0C 00 00 00 00 83 3C CD ?? ?? ?? ?? 00 74 0E 41 89 48 ?? 83 3C CD ?? ?? ?? ?? 00 75 F2 } \ 41 | $v7a = { 6A 06 33 D2 ?? F7 ?? 8B DA 43 74 } \ 42 | $v7b = { 83 E6 0F 8B CF 83 C6 04 50 8B D6 E8 ?? ?? ?? ?? 59 6A 2F 8D 3C 77 58 66 89 07 83 C7 02 4B 75 } \ 43 | condition: all of ($v4*) or $v5a or $v6a or all of ($v7*)}' 44 | } 45 | 46 | # MZ Header 47 | MZ_HEADER = b"\x4D\x5A\x90\x00" 48 | 49 | # Config pattern 50 | CONFIG_PATTERNS = [re.compile("(........)(\x9A\x1F|\x90\x1F|\xBB\x01|\x50\x00|\xA8\x1B|\x2F\x10|\x50\xC3|\xDE\x03|\x2F\x10|\xE3\x03|\x14\x00|\x16\x00)(......)(\x9A\x1F|\x90\x1F|\xBB\x01|\x50\x00|\xA8\x1B|\x2F\x10|\x50\xC3|\xDE\x03|\x2F\x10|\xE3\x03|\x14\x00|\x16\x00)", re.DOTALL), 51 | re.compile("\x00\x00\x00\x00(....)(\x9A\x1F|\x90\x1F|\xBB\x01|\x50\x00|\xA8\x1B|\x2F\x10|\x50\xC3|\xDE\x03|\x2F\x10|\xE3\x03|\x14\x00|\x16\x00)(......)(\x9A\x1F|\x90\x1F|\xBB\x01|\x50\x00|\xA8\x1B|\x2F\x10|\x50\xC3|\xDE\x03|\x2F\x10|\xE3\x03|\x14\x00|\x16\x00)", re.DOTALL)] 52 | 53 | 54 | class emotetConfig(taskmods.DllList): 55 | """Parse the Emotet configuration""" 56 | 57 | @staticmethod 58 | def is_valid_profile(profile): 59 | return (profile.metadata.get('os', 'unknown') == 'windows'), profile.metadata.get('memory_model', '32bit') 60 | 61 | def get_vad_base(self, task, address): 62 | for vad in task.VadRoot.traverse(): 63 | if address >= vad.Start and address < vad.End: 64 | return vad.Start, vad.End 65 | 66 | return None 67 | 68 | def extract_rsakey(self, data): 69 | pubkey = "" 70 | pemkey_match = re.findall('''\x30[\x00-\xFF]{100}\x02\x03\x01\x00\x01\x00\x00''',data) 71 | 72 | if pemkey_match: 73 | pemkey = pemkey_match[0][0:106] 74 | seq = asn1.DerSequence() 75 | seq.decode(pemkey) 76 | pemkey = RSA.construct((seq[0],seq[1])) 77 | pubkey = pemkey.exportKey() 78 | 79 | return pubkey 80 | 81 | def calculate(self): 82 | 83 | if not has_yara: 84 | debug.error("Yara must be installed for this plugin") 85 | 86 | addr_space = utils.load_as(self._config) 87 | 88 | os, memory_model = self.is_valid_profile(addr_space.profile) 89 | if not os: 90 | debug.error("This command does not support the selected profile.") 91 | 92 | rules = yara.compile(sources=emotet_sig) 93 | 94 | for task in self.filter_tasks(tasks.pslist(addr_space)): 95 | scanner = malfind.VadYaraScanner(task=task, rules=rules) 96 | 97 | for hit, address in scanner.scan(): 98 | 99 | vad_base_addr, end = self.get_vad_base(task, address) 100 | proc_addr_space = task.get_process_address_space() 101 | data = proc_addr_space.zread(vad_base_addr, end - vad_base_addr) 102 | 103 | config_data = [] 104 | 105 | p_data = OrderedDict() 106 | for pattern in CONFIG_PATTERNS: 107 | mc = re.search(pattern, data) 108 | if mc: 109 | try: 110 | d = 4 111 | i = 0 112 | while 1: 113 | ip = data[mc.start() + d + 3] + data[mc.start() + d + 2] + data[mc.start() + d + 1] + data[mc.start() + d] 114 | port = unpack("=H", data[mc.start() + d + 4:mc.start() + d + 6])[0] 115 | d += 8 116 | if ip == "\x00\x00\x00\x00" and port == 0: 117 | break 118 | else: 119 | p_data["IP " + str(i)] = str(inet_ntoa(ip)) + ":" + str(port) 120 | i += 1 121 | except: 122 | outfd.write("[!] Not found config data.\n") 123 | 124 | config_data.append({"RSA Public Key" : self.extract_rsakey(data)}) 125 | config_data.append(p_data) 126 | 127 | yield task, vad_base_addr, end, hit, memory_model, config_data 128 | break 129 | 130 | def render_text(self, outfd, data): 131 | 132 | delim = '-' * 70 133 | 134 | for task, start, end, malname, memory_model, config_data in data: 135 | outfd.write("{0}\n".format(delim)) 136 | outfd.write("Process: {0} ({1})\n\n".format(task.ImageFileName, task.UniqueProcessId)) 137 | 138 | outfd.write("[Static IP Address list]\n") 139 | for p_data in config_data: 140 | for id, param in p_data.items(): 141 | outfd.write("{0}:{1}\n".format(id, param)) 142 | -------------------------------------------------------------------------------- /utils/lokibotscan.py: -------------------------------------------------------------------------------- 1 | # Detecting LokiBot for Volatility 2 | # 3 | # LICENSE 4 | # Please refer to the LICENSE.txt in the https://github.com/JPCERTCC/MalConfScan/ 5 | # 6 | # How to use: 7 | # 1. cd "Volatility Folder" 8 | # 2. mv lokibotscan.py volatility/plugins/malware 9 | # 3. python vol.py lokibotconfig -f images.mem --profile=Win7SP1x64 10 | 11 | import volatility.plugins.taskmods as taskmods 12 | import volatility.win32.tasks as tasks 13 | import volatility.utils as utils 14 | import volatility.debug as debug 15 | import volatility.plugins.malware.malfind as malfind 16 | import re 17 | from base64 import b64decode 18 | from collections import OrderedDict 19 | 20 | try: 21 | import yara 22 | has_yara = True 23 | except ImportError: 24 | has_yara = False 25 | 26 | try: 27 | from Crypto.Cipher import DES3 28 | has_crypto = True 29 | except ImportError: 30 | has_crypto = False 31 | 32 | lokibot_sig = { 33 | 'namespace1' : 'rule Lokibot { \ 34 | strings: \ 35 | $des3 = { 68 03 66 00 00 } \ 36 | $param = "MAC=%02X%02X%02XINSTALL=%08X%08X" \ 37 | $string = { 2d 00 75 00 00 00 46 75 63 6b 61 76 2e 72 75 00 00} \ 38 | condition: all of them}' 39 | } 40 | 41 | # Config pattern 42 | CONF_PATTERNS = [re.compile("(..)\x0F\x84(......)\xe9(....)\x90\x90\x90\x90\x90\x90", re.DOTALL)] 43 | 44 | 45 | class lokibotConfig(taskmods.DllList): 46 | """Parse the Lokibot configuration""" 47 | 48 | @staticmethod 49 | def is_valid_profile(profile): 50 | return (profile.metadata.get('os', 'unknown') == 'windows'), profile.metadata.get('memory_model', '32bit') 51 | 52 | def get_vad_base(self, task, address): 53 | for vad in task.VadRoot.traverse(): 54 | if address >= vad.Start and address < vad.End: 55 | return vad.Start, vad.End 56 | 57 | return None 58 | 59 | def string_print(self, line): 60 | try: 61 | return "".join((char for char in line if 32 < ord(char) < 127)) 62 | except: 63 | return line 64 | 65 | def config_decode(self, name, data, config_index, enc_data_count): 66 | enc_data = [] 67 | key_data = [] 68 | enc_set = [] 69 | p_data = OrderedDict() 70 | x = 0 71 | for i in range(enc_data_count): 72 | while 1: 73 | if data[config_index + x] != "\0": 74 | enc_set.append(data[config_index + x]) 75 | x += 1 76 | else: 77 | enc_data.append("".join(enc_set)) 78 | enc_set = [] 79 | x += 4 80 | break 81 | 82 | config_index = config_index + x 83 | iv = data[config_index:config_index + 12].replace("\0", "") 84 | 85 | config_index = config_index + 12 86 | for i in range(3)[::-1]: 87 | key_data.append(data[config_index + (12 * i):config_index + (12 * (i + 1))].replace("\0", "")) 88 | 89 | key = "".join(key_data) 90 | i = 0 91 | for data in enc_data: 92 | des = DES3.new(key, IV=iv, mode=DES3.MODE_CBC) 93 | data_dec = des.decrypt(data) 94 | p_data[name + " " + str(i)] = self.string_print(data_dec) 95 | i += 1 96 | 97 | return p_data 98 | 99 | def calculate(self): 100 | 101 | if not has_yara: 102 | debug.error("Yara must be installed for this plugin") 103 | 104 | if not has_crypto: 105 | debug.error("pycrypto must be installed for this plugin") 106 | 107 | addr_space = utils.load_as(self._config) 108 | 109 | os, memory_model = self.is_valid_profile(addr_space.profile) 110 | if not os: 111 | debug.error("This command does not support the selected profile.") 112 | 113 | rules = yara.compile(sources=lokibot_sig) 114 | 115 | for task in self.filter_tasks(tasks.pslist(addr_space)): 116 | scanner = malfind.VadYaraScanner(task=task, rules=rules) 117 | 118 | for hit, address in scanner.scan(): 119 | 120 | vad_base_addr, end = self.get_vad_base(task, address) 121 | proc_addr_space = task.get_process_address_space() 122 | data = proc_addr_space.zread(vad_base_addr, end - vad_base_addr) 123 | 124 | config_data = [] 125 | 126 | config_index = data.find("ckav.ru") + 12 127 | config_data.append(self.config_decode("Original URL", data, config_index, 4)) 128 | config_index = data.find("INSTALL=%08X%08X") + 16 129 | config_data.append(self.config_decode("Registry key", data, config_index, 1)) 130 | 131 | for pattern in CONF_PATTERNS: 132 | mk = re.search(pattern, data) 133 | 134 | enc_set = [] 135 | x = 0 136 | if mk: 137 | if "h" in data[mk.start() + 0x30]: 138 | key = 0x0 139 | else: 140 | key = 0xFF 141 | 142 | while 1: 143 | if data[mk.start() + 0x30 + x] != "\0": 144 | enc_set.append(chr(ord(data[mk.start() + 0x30 + x]) ^ key)) 145 | x += 1 146 | else: 147 | enc_data = "".join(enc_set) 148 | break 149 | 150 | p_data = {} 151 | p_data["Setting URL"] = self.string_print(enc_data) 152 | config_data.append(p_data) 153 | 154 | yield task, vad_base_addr, end, hit, memory_model, config_data 155 | break 156 | 157 | def render_text(self, outfd, data): 158 | 159 | delim = '-' * 70 160 | 161 | for task, start, end, malname, memory_model, config_data in data: 162 | outfd.write("{0}\n".format(delim)) 163 | outfd.write("Process: {0} ({1})\n\n".format(task.ImageFileName, task.UniqueProcessId)) 164 | 165 | outfd.write("[Config Info]\n") 166 | for p_data in config_data: 167 | for id, param in p_data.items(): 168 | outfd.write("{0:<16}: {1}\n".format(id, param)) 169 | -------------------------------------------------------------------------------- /utils/wellmessscan.py: -------------------------------------------------------------------------------- 1 | # Detecting Wellmess for Volatility 2 | # 3 | # LICENSE 4 | # Please refer to the LICENSE.txt in the https://github.com/JPCERTCC/MalConfScan/ 5 | # 6 | # How to use: 7 | # 1. cd "Volatility Folder" 8 | # 2. mv wellmessscan.py volatility/plugins/malware 9 | # 3. python vol.py wellmessconfig -f images.mem --profile=Win7SP1x64 10 | 11 | import volatility.plugins.taskmods as taskmods 12 | import volatility.win32.tasks as tasks 13 | import volatility.utils as utils 14 | import volatility.debug as debug 15 | import volatility.plugins.malware.malfind as malfind 16 | import re 17 | import pefile 18 | from struct import unpack, unpack_from 19 | from collections import OrderedDict 20 | 21 | try: 22 | import yara 23 | has_yara = True 24 | except ImportError: 25 | has_yara = False 26 | 27 | wellmess_sig = { 28 | 'namespace1' : 'rule Wellmess { \ 29 | strings: \ 30 | $botlib1 = "botlib.wellMess" ascii\ 31 | $botlib2 = "botlib.Command" ascii\ 32 | $botlib3 = "botlib.Download" ascii\ 33 | $botlib4 = "botlib.AES_Encrypt" ascii\ 34 | $dotnet1 = "WellMess" ascii\ 35 | $dotnet2 = "<;head;><;title;>" ascii wide\ 36 | $dotnet3 = "<;title;><;service;>" ascii wide\ 37 | $dotnet4 = "AES_Encrypt" ascii\ 38 | condition: (uint16(0) == 0x5A4D) and (all of ($botlib*) or all of ($dotnet*))}' 39 | } 40 | 41 | # Config pattern 42 | CONFIG_PATTERNS = [re.compile("\x00(.)\x00\x00\x00\x8B\x05...\x00\x85\xC0\x0F\x85..\x00\x00\x8D\x05(....)\x89\x05...\x00\xC7\x05", re.DOTALL), 43 | re.compile("\x00(.)\x00\x00\x00\x8B\x05...\x00\x85\xC0\x0F\x85..\x00\x00\x48\x8D\x05(....)\x48\x89\x05...\x00\x48\xC7\x05", re.DOTALL)] 44 | 45 | CONFIG_PATTERNS_DOTNET = [re.compile("\x00\x0B\x61\x00\x3A\x00\x31\x00\x5F\x00\x30\x00\x00\x0B\x61\x00\x3A\x00\x31\x00\x5F\x00\x31\x00\x00", re.DOTALL)] 46 | 47 | class wellmessConfig(taskmods.DllList): 48 | """Parse the Wellmess configuration""" 49 | 50 | @staticmethod 51 | def is_valid_profile(profile): 52 | return (profile.metadata.get('os', 'unknown') == 'windows'), profile.metadata.get('memory_model', '32bit') 53 | 54 | def get_vad_base(self, task, address): 55 | for vad in task.VadRoot.traverse(): 56 | if address >= vad.Start and address < vad.End: 57 | return vad.Start, vad.End 58 | 59 | return None 60 | 61 | def parse_config(self, config): 62 | p_data = OrderedDict() 63 | for i, d in enumerate(config): 64 | p_data["conf " + str(i)] = d 65 | 66 | return p_data 67 | 68 | def calculate(self): 69 | 70 | if not has_yara: 71 | debug.error("Yara must be installed for this plugin") 72 | 73 | addr_space = utils.load_as(self._config) 74 | 75 | os, memory_model = self.is_valid_profile(addr_space.profile) 76 | if not os: 77 | debug.error("This command does not support the selected profile.") 78 | 79 | rules = yara.compile(sources=wellmess_sig) 80 | 81 | for task in self.filter_tasks(tasks.pslist(addr_space)): 82 | scanner = malfind.VadYaraScanner(task=task, rules=rules) 83 | 84 | for hit, address in scanner.scan(): 85 | 86 | vad_base_addr, end = self.get_vad_base(task, address) 87 | proc_addr_space = task.get_process_address_space() 88 | data = proc_addr_space.zread(vad_base_addr, end - vad_base_addr) 89 | 90 | pe = pefile.PE(data=data) 91 | 92 | config_data = [] 93 | configs = [] 94 | for pattern in CONFIG_PATTERNS: 95 | mc = list(re.finditer(pattern, data)) 96 | if mc: 97 | for m in mc: 98 | hit_adderss = m.span() 99 | config_rva = unpack("=I", m.groups()[1])[0] 100 | 101 | if pe.FILE_HEADER.Machine == 0x14C: # for 32bit 102 | config_offset = config_rva - pe.NT_HEADERS.OPTIONAL_HEADER.ImageBase 103 | #config_offset = pe.get_physical_by_rva(config_rva - pe.NT_HEADERS.OPTIONAL_HEADER.ImageBase) + 0x1000 104 | else: # for 64bit 105 | config_offset = config_rva + hit_adderss[0] + 26 106 | 107 | configs.append(data[config_offset:config_offset + ord(m.groups()[0])]) 108 | 109 | for pattern in CONFIG_PATTERNS_DOTNET: 110 | mc = re.search(pattern, data) 111 | if mc: 112 | offset = mc.end() 113 | for i in range(6): 114 | strings = [] 115 | string_len = ord(data[offset]) 116 | 117 | if ord(data[offset]) == 0x80 or ord(data[offset]) == 0x83: 118 | string_len = ord(data[offset + 1]) + ((ord(data[offset]) - 0x80) * 256) 119 | offset += 1 120 | 121 | offset += 1 122 | for i in range(string_len): 123 | if data[offset + i] != "\x00": 124 | strings.append(data[offset + i]) 125 | if string_len != 1: 126 | configs.append("".join(strings)) 127 | offset = offset + string_len 128 | 129 | config_data.append(self.parse_config(configs)) 130 | 131 | yield task, vad_base_addr, end, hit, memory_model, config_data 132 | break 133 | 134 | def render_text(self, outfd, data): 135 | 136 | delim = '-' * 70 137 | 138 | for task, start, end, malname, memory_model, config_data in data: 139 | outfd.write("{0}\n".format(delim)) 140 | outfd.write("Process: {0} ({1})\n\n".format(task.ImageFileName, task.UniqueProcessId)) 141 | 142 | outfd.write("[Config Info]\n") 143 | for p_data in config_data: 144 | for id, param in p_data.items(): 145 | outfd.write("{0:<25}: {1}\n".format(id, param)) 146 | -------------------------------------------------------------------------------- /utils/trickbotscan.py: -------------------------------------------------------------------------------- 1 | # Detecting TrickBot for Volatilitv 2 | # 3 | # LICENSE 4 | # Please refer to the LICENSE.txt in the https://github.com/JPCERTCC/MalConfScan/ 5 | # 6 | # How to use: 7 | # 1. cd "Volatility Folder" 8 | # 2. mv trickbotconfigallocate.py volatility/plugins/malware 9 | # 3. python vol.py trickbotconfig -f images.mem --profile=Win7SP1x64 10 | 11 | import volatility.plugins.taskmods as taskmods 12 | import volatility.win32.tasks as tasks 13 | import volatility.utils as utils 14 | import volatility.debug as debug 15 | import volatility.plugins.malware.malfind as malfind 16 | import re 17 | import xml.etree.ElementTree as ET 18 | from collections import OrderedDict 19 | from struct import unpack, unpack_from 20 | 21 | try: 22 | import yara 23 | has_yara = True 24 | except ImportError: 25 | has_yara = False 26 | 27 | trickbot_sig = { 28 | 'namespace1' : 'rule Trickbot { \ 29 | strings: \ 30 | $tagm1 = "" wide \ 31 | $tagm2 = "" wide \ 32 | $tagc1 = "" wide \ 33 | $tagc2 = "" wide \ 34 | $tagi1 = "" wide \ 35 | $tagi2 = "" wide \ 36 | $tags1 = "" wide \ 37 | $tags2 = "" wide \ 38 | $tagl1 = "" wide \ 39 | $tagl2 = "" wide \ 40 | condition: all of ($tagm*) or all of ($tagc*) or all of ($tagi*) or all of ($tags*) or all of ($tagl*)}' 41 | } 42 | 43 | # Config pattern 44 | CONFIG_PATTERNS = [re.compile("\x3C\x00\x6D\x00\x63\x00\x63\x00\x6F\x00\x6E\x00\x66\x00\x3E\x00\x3C\x00\x76\x00\x65\x00\x72\x00\x3E\x00(.*)\x3C\x00\x2F\x00\x6D\x00\x63\x00\x63\x00\x6F\x00\x6E\x00\x66\x00\x3E\x00", re.DOTALL), 45 | re.compile("\x3C\x00\x6D\x00\x6F\x00\x64\x00\x75\x00\x6C\x00\x65\x00\x63\x00\x6F\x00\x6E\x00\x66\x00\x69\x00\x67\x00\x3E\x00\x3C\x00\x61\x00\x75\x00\x74\x00\x6F\x00\x73\x00\x74\x00\x61\x00\x72\x00\x74\x00\x3E\x00(.*)\x3C\x00\x2F\x00\x61\x00\x75\x00\x74\x00\x6F\x00\x63\x00\x6F\x00\x6E\x00\x66\x00\x3E\x00\x3C\x00\x2F\x00\x6D\x20\x6F\x00\x64\x00\x75\x00\x6C\x00\x65\x00\x63\x00\x6F\x00\x6E\x00\x66\x00\x69\x00\x67\x00\x3E\x00", re.DOTALL), 46 | re.compile("\x3C\x00\x69\x00\x67\x00\x72\x00\x6F\x00\x75\x00\x70\x00\x3E\x00\x3C\x00\x64\x00\x69\x00\x6E\x00\x6A\x00\x3E\x00(.*)\x3C\x00\x2F\x00\x64\x00\x69\x00\x6E\x00\x6A\x00\x3E\x00\x3C\x00\x2F\x00\x69\x00\x67\x00\x72\x00\x6F\x00\x75\x00\x70\x00\x3E\x00", re.DOTALL), 47 | re.compile("\x3C\x00\x73\x00\x65\x00\x72\x00\x76\x00\x63\x00\x6F\x00\x6E\x00\x66\x00\x3E\x00\x3C\x00\x65\x00\x78\x00\x70\x00\x69\x00\x72\x00\x3E\x00(.*)\x3C\x00\x2F\x00\x70\x00\x6C\x00\x75\x00\x67\x00\x69\x00\x6E\x00\x73\x00\x3E\x00\x3C\x00\x2F\x00\x73\x00\x65\x00\x72\x00\x76\x00\x63\x00\x6F\x00\x6E\x00\x66\x00\x3E\x00", re.DOTALL), 48 | re.compile("\x3C\x00\x73\x00\x6C\x00\x69\x00\x73\x00\x74\x00\x3E\x00\x3C\x00\x73\x00\x69\x00\x6E\x00\x6A\x00\x3E\x00(.*)\x3C\x00\x2F\x00\x73\x00\x69\x00\x6E\x00\x6A\x00\x3E\x00\x3C\x00\x2F\x00\x73\x00\x6C\x00\x69\x00\x73\x00\x74\x00\x3E\x00", re.DOTALL)] 49 | 50 | 51 | class trickbotConfig(taskmods.DllList): 52 | "Parse the TrickBot configuration" 53 | 54 | @staticmethod 55 | def is_valid_profile(profile): 56 | return (profile.metadata.get('os', 'unknown') == 'windows'), profile.metadata.get('memory_model', '32bit') 57 | 58 | def get_vad_base(self, task, address): 59 | for vad in task.VadRoot.traverse(): 60 | if address >= vad.Start and address < vad.End: 61 | return vad.Start, vad.End 62 | return None 63 | 64 | def calculate(self): 65 | 66 | if not has_yara: 67 | debug.error('Yara must be installed for this plugin.') 68 | 69 | addr_space = utils.load_as(self._config) 70 | 71 | os, memory_model = self.is_valid_profile(addr_space.profile) 72 | if not os: 73 | debug.error('This command does not support the selected profile.') 74 | 75 | rules = yara.compile(sources=trickbot_sig) 76 | 77 | for task in self.filter_tasks(tasks.pslist(addr_space)): 78 | scanner = malfind.VadYaraScanner(task=task, rules=rules) 79 | for hit, address in scanner.scan(): 80 | 81 | vad_base_addr, end = self.get_vad_base(task, address) 82 | proc_addr_space = task.get_process_address_space() 83 | memdata = proc_addr_space.get_available_addresses() 84 | 85 | config_data = [] 86 | 87 | for m in memdata: 88 | 89 | if m[1] <= 0x1000: 90 | continue 91 | 92 | data = proc_addr_space.zread(m[0], m[1]) 93 | 94 | for pattern in CONFIG_PATTERNS: 95 | m = re.search(pattern, data) 96 | 97 | if m: 98 | offset = m.start() 99 | else: 100 | continue 101 | 102 | p_data = OrderedDict() 103 | xml_data = data[offset:m.end()] 104 | root = ET.fromstring(xml_data) 105 | i = 0 106 | for e in root.getiterator(): 107 | if e.text is None: 108 | if len(e.attrib) != 0: 109 | p_data[i] = e.tag + ": " + str(e.attrib) 110 | else: 111 | p_data[i] = e.tag + ": " + str(e.text) 112 | i += 1 113 | config_data.append(p_data) 114 | 115 | yield task, vad_base_addr, end, hit, memory_model, config_data 116 | break 117 | 118 | def render_text(self, outfd, data): 119 | 120 | delim = '-' * 70 121 | 122 | for task, start, end, malname, memory_model, config_data in data: 123 | outfd.write("{0}\n".format(delim)) 124 | outfd.write("Process: {0} ({1})\n\n".format(task.ImageFileName, task.UniqueProcessId)) 125 | 126 | outfd.write("[Config Info]\n") 127 | for p_data in config_data: 128 | for id, param in p_data.items(): 129 | outfd.write("{0:<4}: {1}\n".format(id, param)) 130 | -------------------------------------------------------------------------------- /utils/agentteslascan.py: -------------------------------------------------------------------------------- 1 | # Detecting AgentTesla Keylogger for Volatility 2 | # 3 | # LICENSE 4 | # Please refer to the LICENSE.txt in the https://github.com/JPCERTCC/MalConfScan/ 5 | # 6 | # How to use: 7 | # 1. cd "Volatility Folder" 8 | # 2. mv agentteslascan.py volatility/plugins/malware 9 | # 3. python vol.py agentteslaconfig -f images.mem --profile=Win7SP1x64 10 | 11 | import volatility.plugins.taskmods as taskmods 12 | import volatility.win32.tasks as tasks 13 | import volatility.utils as utils 14 | import volatility.debug as debug 15 | import volatility.plugins.malware.malfind as malfind 16 | import re 17 | from base64 import b64decode 18 | from collections import OrderedDict 19 | 20 | try: 21 | import yara 22 | has_yara = True 23 | except ImportError: 24 | has_yara = False 25 | 26 | try: 27 | from Crypto.Cipher import AES 28 | has_crypto = True 29 | except ImportError: 30 | has_crypto = False 31 | 32 | agenttesla_sig = { 33 | 'namespace1' : 'rule Agenttesla_type1 { \ 34 | strings: \ 35 | $type1ie = "C:\\\\Users\\\\Admin\\\\Desktop\\\\IELibrary\\\\IELibrary\\\\obj\\\\Debug\\\\IELibrary.pdb" \ 36 | $type1at = "C:\\\\Users\\\\Admin\\\\Desktop\\\\ConsoleApp1\\\\ConsoleApp1\\\\obj\\\\Debug\\\\ConsoleApp1.pdb" \ 37 | $type1sql = "Not a valid SQLite 3 Database File" wide \ 38 | condition: all of them}', 39 | 'namespace2' : 'rule Agenttesla_type2 { \ 40 | strings: \ 41 | $type2db1 = "1.85 (Hash, version 2, native byte-order)" wide \ 42 | $type2db2 = "Unknow database format" wide \ 43 | $type2db3 = "SQLite format 3" wide \ 44 | $type2db4 = "Berkelet DB" wide \ 45 | condition: (uint16(0) == 0x5A4D) and 3 of them}' 46 | } 47 | 48 | # IV 49 | IV = "@1B2c3D4e5F6g7H8" 50 | 51 | # AES Key 52 | KEY = "\x34\x88\x6D\x5B\x09\x7A\x94\x19\x78\xD0\xE3\x8b\x1b\x5c\xa3\x29\x60\x74\x6a\x5e\x5d\x64\x87\x11\xb1\x2c\x67\xaa\x5b\x3a\x8e\xbf" 53 | 54 | 55 | class agentteslaConfig(taskmods.DllList): 56 | """Parse the Agenttesla configuration""" 57 | 58 | @staticmethod 59 | def is_valid_profile(profile): 60 | return (profile.metadata.get('os', 'unknown') == 'windows'), profile.metadata.get('memory_model', '32bit') 61 | 62 | def get_vad_base(self, task, address): 63 | for vad in task.VadRoot.traverse(): 64 | if address >= vad.Start and address < vad.End: 65 | return vad.Start, vad.End 66 | 67 | return None 68 | 69 | def base64strings(self, data, n=18): 70 | for match in re.finditer(("(([0-9a-z-A-Z\+/]\x00){%s}([0-9a-z-A-Z\+/]\x00)*(=\x00){0,2})" % n).encode(), data): 71 | yield match.group(0) 72 | 73 | def remove_unascii(self, b): 74 | cleaned = "" 75 | for i in b: 76 | if ord(i) >= 0x20 and ord(i) < 0x7f: 77 | cleaned += i 78 | return cleaned 79 | 80 | def stringdecrypt_type1(self, a): 81 | string = b64decode(a) 82 | cleartext = AES.new(KEY[0:32], AES.MODE_CBC, IV).decrypt(string) 83 | return cleartext 84 | 85 | def stringdecrypt_type2(self, data): 86 | encdata = data[0x2050:] 87 | 88 | dlist = OrderedDict() 89 | offset = 0 90 | num = 0 91 | i = 16 92 | while True: 93 | key = encdata[offset:offset + 32] 94 | iv = encdata[offset + 32:offset + 48] 95 | enc_data =encdata[offset + 48:offset + 48 + i] 96 | 97 | if b"\x00\x00" in key and b"\x00\x00" in iv: 98 | break 99 | 100 | try: 101 | cleartext = AES.new(key, AES.MODE_CBC, iv).decrypt(enc_data) 102 | if len(cleartext) and (ord(cleartext[-1]) <= 0x10 or self.remove_unascii(cleartext) % 16 == 0) and not (ord(cleartext[-2]) == 0x0d and ord(cleartext[-1]) == 0x0a): 103 | dlist["Encoded string " + str(num)] = self.remove_unascii(cleartext).rstrip() 104 | offset = offset + 48 + i 105 | num += 1 106 | i = 0 107 | else: 108 | i += 16 109 | except: 110 | i += 16 111 | 112 | return dlist 113 | 114 | def calculate(self): 115 | 116 | if not has_yara: 117 | debug.error("Yara must be installed for this plugin") 118 | 119 | if not has_crypto: 120 | debug.error("pycrypto must be installed for this plugin") 121 | 122 | addr_space = utils.load_as(self._config) 123 | 124 | os, memory_model = self.is_valid_profile(addr_space.profile) 125 | if not os: 126 | debug.error("This command does not support the selected profile.") 127 | 128 | rules = yara.compile(sources=agenttesla_sig) 129 | 130 | for task in self.filter_tasks(tasks.pslist(addr_space)): 131 | scanner = malfind.VadYaraScanner(task=task, rules=rules) 132 | 133 | for hit, address in scanner.scan(): 134 | 135 | vad_base_addr, end = self.get_vad_base(task, address) 136 | proc_addr_space = task.get_process_address_space() 137 | data = proc_addr_space.zread(vad_base_addr, end - vad_base_addr) 138 | 139 | config_data = [] 140 | dlist = OrderedDict() 141 | if "type1" in str(hit): 142 | for word in self.base64strings(data): 143 | try: 144 | dec = self.stringdecrypt_type1(word) 145 | dec = self.remove_unascii(dec).rstrip() 146 | dlist[word.strip().replace('\0', '')] = dec 147 | except: 148 | pass 149 | 150 | if "type2" in str(hit): 151 | dlist = self.stringdecrypt_type2(data) 152 | 153 | config_data.append(dlist) 154 | 155 | yield task, vad_base_addr, end, hit, memory_model, config_data 156 | break 157 | 158 | def render_text(self, outfd, data): 159 | 160 | delim = '-' * 70 161 | 162 | for task, start, end, malname, memory_model, config_data in data: 163 | outfd.write("{0}\n".format(delim)) 164 | outfd.write("Process: {0} ({1})\n\n".format(task.ImageFileName, task.UniqueProcessId)) 165 | 166 | outfd.write("[Config Info]\n") 167 | for p_data in config_data: 168 | for id, param in p_data.items(): 169 | outfd.write("{0:<25}: {1}\n".format(id, param)) 170 | -------------------------------------------------------------------------------- /utils/elf_pleadscan.py: -------------------------------------------------------------------------------- 1 | # Detecting ELF_PLEAD for Volatilitv 2 | # 3 | # LICENSE 4 | # Please refer to the LICENSE.txt in the https://github.com/JPCERTCC/MalConfScan/ 5 | # 6 | # How to use: 7 | # 1. cd "Volatility Folder" 8 | # 2. mv elf_pleadconfig.py volatility/plugins/malware 9 | # 3. python vol.py elf_pleadconfig -f images.mem --profile=Win7SP1x64 10 | 11 | import volatility.plugins.taskmods as taskmods 12 | import volatility.win32.tasks as tasks 13 | import volatility.utils as utils 14 | import volatility.debug as debug 15 | import volatility.plugins.malware.malfind as malfind 16 | import volatility.plugins.linux.pslist as linux_pslist 17 | import volatility.plugins.linux.linux_yarascan as linux_yarascan 18 | import re 19 | import io 20 | from struct import unpack, unpack_from 21 | from collections import OrderedDict 22 | 23 | try: 24 | import yara 25 | has_yara = True 26 | except ImportError: 27 | has_yara = False 28 | 29 | elf_plead_sig = { 30 | 'namespace1' : 'rule elf_plead { \ 31 | strings: \ 32 | $ioctl = "ioctl TIOCSWINSZ error" \ 33 | $class1 = "CPortForwardManager" \ 34 | $class2 = "CRemoteShell" \ 35 | $class3 = "CFileManager" \ 36 | $lzo = { 81 ?? FF 07 00 00 81 ?? 1F 20 00 00 } \ 37 | condition: 3 of them}' 38 | } 39 | 40 | # Config pattern 41 | CONFIG_PATTERNS = [re.compile("\xBA(...)\x00\xB9\xAA\x01\x00\x00\xBE\x20\x00\x00\x00\xBF(...)\x00", re.DOTALL)] 42 | 43 | CONFIG_SIZE = 0x1AA 44 | KEY_SIZE = 0x20 45 | 46 | class elf_pleadConfig(linux_pslist.linux_pslist): 47 | "Parse the ELF_PLEAD configuration" 48 | 49 | @staticmethod 50 | def is_valid_profile(profile): 51 | return profile.metadata.get('os', 'unknown'), profile.metadata.get('memory_model', '32bit') 52 | 53 | def get_vma_base(self, task, address): 54 | for vma in task.get_proc_maps(): 55 | if address >= vma.vm_start and address < vma.vm_end: 56 | return vma.vm_start, vma.vm_end 57 | 58 | return None 59 | 60 | def filter_tasks(self): 61 | tasks = linux_pslist.linux_pslist(self._config).calculate() 62 | 63 | if self._config.PID is not None: 64 | try: 65 | pidlist = [int(p) for p in self._config.PID.split(',')] 66 | except ValueError: 67 | debug.error("Invalid PID {0}".format(self._config.PID)) 68 | 69 | pids = [t for t in tasks if t.pid in pidlist] 70 | if len(pids) == 0: 71 | debug.error("Cannot find PID {0}. If its terminated or unlinked, use psscan and then supply --offset=OFFSET".format(self._config.PID)) 72 | return pids 73 | 74 | return tasks 75 | 76 | def rc4(self, data, key): 77 | x = 0 78 | box = range(256) 79 | for i in range(256): 80 | x = (x + box[i] + ord(key[i % len(key)])) % 256 81 | box[i], box[x] = box[x], box[i] 82 | x = 0 83 | y = 0 84 | out = [] 85 | for char in data: 86 | x = (x + 1) % 256 87 | y = (y + box[x]) % 256 88 | box[x], box[y] = box[y], box[x] 89 | out.append(chr(ord(char) ^ box[(box[x] + box[y]) % 256])) 90 | 91 | return ''.join(out) 92 | 93 | def parse_config(self, data, start, memory_model): 94 | 95 | p_data = OrderedDict() 96 | 97 | for pattern in CONFIG_PATTERNS: 98 | if "64" in memory_model: 99 | data_base_address = unpack("=Q", data[0x90:0x98])[0] - unpack("=Q", data[0x80:0x88])[0] 100 | else: 101 | data_base_address = unpack("=I", data[0x60:0x64])[0] - unpack("=I", data[0x58:0x5C])[0] 102 | 103 | mc = re.search(pattern, data) 104 | if mc: 105 | config_offset = mc.start(1) 106 | config_address = unpack("=I", data[config_offset:config_offset + 4])[0] - data_base_address 107 | enc_config = data[config_address:config_address + CONFIG_SIZE] 108 | 109 | key_offset = mc.start(2) 110 | key_address = unpack("=I", data[key_offset:key_offset + 4])[0] - data_base_address 111 | key = data[key_address:key_address + KEY_SIZE] 112 | 113 | if enc_config[0] == "\x00": 114 | print("[!] Config area is brank.") 115 | else: 116 | config = self.rc4(enc_config, key) 117 | 118 | p_data["ID"] = unpack_from("<8s", config, 0)[0].replace("\0", "") 119 | p_data["Unknown1"] = u"0x{0:X}".format(unpack_from("=Q", config, 0x8)[0]) 120 | p_data["Unknown2"] = u"0x{0:X}".format(unpack_from("=Q", config, 0x10)[0]) 121 | p_data["Unknown3"] = u"0x{0:X}".format(unpack_from("=Q", config, 0x18)[0]) 122 | p_data["Port1"] = unpack_from("I", config, 0x1A6)[0]) 126 | 127 | return p_data 128 | 129 | def calculate(self): 130 | 131 | if not has_yara: 132 | debug.error('Yara must be installed for this plugin.') 133 | 134 | addr_space = utils.load_as(self._config) 135 | 136 | os, memory_model = self.is_valid_profile(addr_space.profile) 137 | if not os: 138 | debug.error('This command does not support the selected profile.') 139 | 140 | rules = yara.compile(sources=elf_plead_sig) 141 | 142 | for task in self.filter_tasks(): 143 | scanner = linux_yarascan.VmaYaraScanner(task = task, rules = rules) 144 | for hit, address in scanner.scan(): 145 | 146 | start, end = self.get_vma_base(task, address) 147 | data = scanner.address_space.zread(start, (end - start) * 2) 148 | #data = scanner.address_space.zread(address - self._config.REVERSE, self._config.SIZE) 149 | 150 | config_data = [] 151 | config_data.append(self.parse_config(data, start, memory_model)) 152 | 153 | yield task, start, end, hit, memory_model, config_data 154 | break 155 | 156 | def render_text(self, outfd, data): 157 | 158 | delim = '-' * 70 159 | 160 | for task, start, end, malname, memory_model, config_data in data: 161 | outfd.write("{0}\n".format(delim)) 162 | outfd.write("Process: {0} ({1})\n\n".format(task.comm, task.pid)) 163 | 164 | outfd.write("[Config Info]\n") 165 | for p_data in config_data: 166 | for id, param in p_data.items(): 167 | outfd.write("{0:<20}: {1}\n".format(id, param)) 168 | -------------------------------------------------------------------------------- /utils/smokeloaderscan.py: -------------------------------------------------------------------------------- 1 | # Detecting SmokeLoader for Volatility 2 | # 3 | # LICENSE 4 | # Please refer to the LICENSE.txt in the https://github.com/JPCERTCC/MalConfScan/ 5 | # 6 | # How to use: 7 | # 1. cd "Volatility Folder" 8 | # 2. mv smokeloaderscan.py volatility/plugins/malware 9 | # 3. python vol.py smokeloaderconfig -f images.mem --profile=Win7SP1x64 10 | 11 | import volatility.plugins.taskmods as taskmods 12 | import volatility.win32.tasks as tasks 13 | import volatility.utils as utils 14 | import volatility.debug as debug 15 | import volatility.plugins.malware.malfind as malfind 16 | import re 17 | from struct import unpack, unpack_from 18 | from collections import OrderedDict 19 | 20 | try: 21 | import yara 22 | has_yara = True 23 | except ImportError: 24 | has_yara = False 25 | 26 | smokeloader_sig = { 27 | 'namespace1' : 'rule SmokeLoader { \ 28 | strings: \ 29 | $a1 = { B8 25 30 38 58 } \ 30 | $b1 = { 81 3D ?? ?? ?? ?? 25 00 41 00 } \ 31 | $c1 = { C7 ?? ?? ?? 25 73 25 73 } \ 32 | condition: $a1 and $b1 and $c1}' 33 | } 34 | 35 | # Config pattern 36 | CONFIG_PATTERNS = [re.compile("\x68\x58\x02\x00\x00\xFF(.....)\x4E\x75\xF2\x8B", re.DOTALL)] 37 | 38 | STRINGS_PATTERNS = [re.compile("\x57\xBB(....)\x8B(.)\x8B(.)", re.DOTALL)] 39 | 40 | 41 | class smokeloaderConfig(taskmods.DllList): 42 | """Parse the SmokeLoader configuration""" 43 | 44 | @staticmethod 45 | def is_valid_profile(profile): 46 | return (profile.metadata.get('os', 'unknown') == 'windows'), profile.metadata.get('memory_model', '32bit') 47 | 48 | def get_vad_base(self, task, address): 49 | for vad in task.VadRoot.traverse(): 50 | if address >= vad.Start and address < vad.End: 51 | return vad.Start, vad.End 52 | 53 | return None 54 | 55 | # RC4 56 | def rc4(self, data, key): 57 | x = 0 58 | box = range(256) 59 | for i in range(256): 60 | x = (x + box[i] + ord(key[i % len(key)])) % 256 61 | box[i], box[x] = box[x], box[i] 62 | x = 0 63 | y = 0 64 | out = [] 65 | for char in data: 66 | x = (x + 1) % 256 67 | y = (y + box[x]) % 256 68 | box[x], box[y] = box[y], box[x] 69 | out.append(chr(ord(char) ^ box[(box[x] + box[y]) % 256])) 70 | 71 | return ''.join(out) 72 | 73 | def decode(self, data, keydata): 74 | url = [] 75 | key = 0xff 76 | for i in range(0, 4): 77 | key = key ^ (keydata >> (i * 8) & 0xff) 78 | for y in data: 79 | url.append(chr(ord(y) ^ key)) 80 | 81 | return "".join(url) 82 | 83 | def calculate(self): 84 | 85 | if not has_yara: 86 | debug.error("Yara must be installed for this plugin") 87 | 88 | addr_space = utils.load_as(self._config) 89 | 90 | os, memory_model = self.is_valid_profile(addr_space.profile) 91 | if not os: 92 | debug.error("This command does not support the selected profile.") 93 | 94 | rules = yara.compile(sources=smokeloader_sig) 95 | 96 | for task in self.filter_tasks(tasks.pslist(addr_space)): 97 | scanner = malfind.VadYaraScanner(task=task, rules=rules) 98 | 99 | for hit, address in scanner.scan(): 100 | 101 | vad_base_addr, end = self.get_vad_base(task, address) 102 | proc_addr_space = task.get_process_address_space() 103 | dll_data = proc_addr_space.zread(vad_base_addr, end - vad_base_addr) 104 | 105 | config_data = [] 106 | 107 | mz_magic = unpack_from("=2s", dll_data, 0x0)[0] 108 | nt_magic = unpack_from("= vad.Start and address < vad.End: 100 | return vad.Start, vad.End 101 | 102 | return None 103 | 104 | def string_print(self, line): 105 | try: 106 | return "".join((char for char in line if 32 < ord(char) < 127)) 107 | except: 108 | return line 109 | 110 | def decrypt_string(self, key, salt, coded): 111 | generator = PBKDF2(key, salt) 112 | aes_iv = generator.read(16) 113 | aes_key = generator.read(32) 114 | 115 | mode = AES.MODE_CBC 116 | cipher = AES.new(aes_key, mode, IV=aes_iv) 117 | value = cipher.decrypt(b64decode(coded)).replace('\x00', '') 118 | return self.string_print(value) 119 | 120 | def parse_config(self, configs): 121 | i = 0 122 | p_data = OrderedDict() 123 | key, salt = 'HawkEyeKeylogger', '3000390039007500370038003700390037003800370038003600'.decode('hex') 124 | for config in configs: 125 | if i in [0, 1, 2, 6, 7, 8, 9]: 126 | config = self.decrypt_string(key, salt, config) 127 | p_data[idx_list[i]] = config 128 | i += 1 129 | 130 | return p_data 131 | 132 | def calculate(self): 133 | 134 | if not has_yara: 135 | debug.error("Yara must be installed for this plugin") 136 | 137 | if not has_crypto: 138 | debug.error("pycrypto must be installed for this plugin") 139 | 140 | if not has_pbkdf2: 141 | debug.error("pbkdf2 must be installed for this plugin") 142 | 143 | addr_space = utils.load_as(self._config) 144 | 145 | os, memory_model = self.is_valid_profile(addr_space.profile) 146 | if not os: 147 | debug.error("This command does not support the selected profile.") 148 | 149 | rules = yara.compile(sources=hawkeye_sig) 150 | 151 | for task in self.filter_tasks(tasks.pslist(addr_space)): 152 | scanner = malfind.VadYaraScanner(task=task, rules=rules) 153 | 154 | for hit, address in scanner.scan(): 155 | 156 | vad_base_addr, end = self.get_vad_base(task, address) 157 | proc_addr_space = task.get_process_address_space() 158 | data = proc_addr_space.zread(vad_base_addr, end - vad_base_addr) 159 | 160 | config_data = [] 161 | 162 | offset = 0 163 | for pattern in CONFIG_PATTERNS: 164 | mc = re.search(pattern, data) 165 | if mc: 166 | offset = mc.end() 167 | 168 | configs = [] 169 | if offset > 0: 170 | while 1: 171 | strings = [] 172 | string_len = ord(data[offset]) 173 | if data[offset] == "\x80": 174 | string_len = ord(data[offset + 1]) 175 | offset += 1 176 | offset += 1 177 | for i in range(string_len): 178 | if data[offset + i] != "\x00": 179 | strings.append(data[offset + i]) 180 | configs.append("".join(strings)) 181 | offset = offset + string_len 182 | if len(configs) > 35: 183 | break 184 | 185 | if not configs[13].isdigit(): 186 | configs.insert(13, 0) 187 | configs.pop(-1) 188 | 189 | config_data.append(self.parse_config(configs)) 190 | 191 | yield task, vad_base_addr, end, hit, memory_model, config_data 192 | break 193 | 194 | def render_text(self, outfd, data): 195 | 196 | delim = '-' * 70 197 | 198 | for task, start, end, malname, memory_model, config_data in data: 199 | outfd.write("{0}\n".format(delim)) 200 | outfd.write("Process: {0} ({1})\n\n".format(task.ImageFileName, task.UniqueProcessId)) 201 | 202 | outfd.write("[Config Info]\n") 203 | for p_data in config_data: 204 | for id, param in p_data.items(): 205 | outfd.write("{0:<21}: {1}\n".format(id, param)) 206 | -------------------------------------------------------------------------------- /utils/xxmmscan.py: -------------------------------------------------------------------------------- 1 | # Detecting xxmm config for Volatilitv 2 | # 3 | # LICENSE 4 | # Please refer to the LICENSE.txt in the https://github.com/JPCERTCC/MalConfScan/ 5 | # 6 | # How to use: 7 | # 1. cd "Volatility Folder" 8 | # 2. mv xxmmconfig.py volatility/plugins/malware 9 | # 3. python vol.py xxmmconfig -f images.mem --profile=Win7SP1x64 10 | 11 | import volatility.plugins.taskmods as taskmods 12 | import volatility.win32.tasks as tasks 13 | import volatility.utils as utils 14 | import volatility.debug as debug 15 | import volatility.plugins.malware.malfind as malfind 16 | import re 17 | from struct import unpack, unpack_from 18 | 19 | try: 20 | import yara 21 | has_yara = True 22 | except ImportError: 23 | has_yara = False 24 | 25 | xxmm_sig = { 26 | 'namespace1' : 'rule xxmm { \ 27 | strings: \ 28 | $v1 = "setupParameter:" \ 29 | $v2 = "loaderParameter:" \ 30 | $v3 = "parameter:" \ 31 | condition: all of them}' 32 | } 33 | 34 | DATA_TYPE = {0x10001: 'ASCII', 35 | 0x104DB: 'UTF-16LE', 36 | 0x104DC: 'UTF-16LE', 37 | 0x104DE: 'ASCII', 38 | 0x104DF: 'UTF-16LE', 39 | 0x104E0: 'UTF-16LE', 40 | 0x104E1: 'ASCII', 41 | 0x104E2: 'ASCII', 42 | 0x104E3: 'ASCII', 43 | 0x104E4: 'ASCII', 44 | 0x104E5: 'UTF-16LE', 45 | 0x104E6: 'UTF-16LE', 46 | 0x104E7: 'UTF-16LE', 47 | 0x104E8: 'UTF-16LE', 48 | 0x104E9: 'UTF-16LE', 49 | 0x104EA: 'UTF-16LE', 50 | 0x10502: 'ASCII', 51 | 0x10515: 'UTF-16LE', 52 | 0x10516: 'UTF-16LE', 53 | 0x10517: 'UTF-16LE', 54 | 0x10518: 'ASCII', 55 | 0x10519: 'UTF-16LE', 56 | 0x1051A: 'UTF-16LE', 57 | 0x1051B: 'UTF-16LE', 58 | 0x1051C: 'UTF-16LE', 59 | 0x1051D: 'UTF-16LE', 60 | 0x1051E: 'UTF-16LE', 61 | 0x1051F: 'UTF-16LE', 62 | 0x10522: 'UTF-16LE', 63 | 0x10525: 'UTF-16LE', 64 | 0x10534: 'UTF-16LE', 65 | 0x10535: 'UTF-16LE', 66 | 0x1053C: 'ASCII', 67 | 0x20520: 'DWORD', 68 | 0x20521: 'DWORD', 69 | 0x20523: 'DWORD', 70 | 0x20524: 'DWORD', 71 | 0x20526: 'DWORD', 72 | 0x20535: 'DWORD', 73 | 0x40500: 'BYTE', 74 | 0x40501: 'BYTE', 75 | 0x80503: 'BYTE', 76 | 0x80514: 'BYTE', 77 | 0x8052A: 'BYTE'} 78 | 79 | 80 | class xxmmConfig(taskmods.DllList): 81 | "Parse the xxmm configuration" 82 | 83 | @staticmethod 84 | def is_valid_profile(profile): 85 | return (profile.metadata.get('os', 'unknown') == 'windows'), profile.metadata.get('memory_model', '32bit') 86 | 87 | def get_vad_base(self, task, address): 88 | for vad in task.VadRoot.traverse(): 89 | if address >= vad.Start and address < vad.End: 90 | return vad.Start, vad.End 91 | return None 92 | 93 | def extract_param(self, conf_data, offset): 94 | l = unpack_from('>I', conf_data, offset)[0] 95 | if 8 <= l <= len(conf_data[offset:]): 96 | idnum = unpack_from('>I', conf_data, offset + 0x4)[0] 97 | s = conf_data[offset + 0x8:offset + l] 98 | else: 99 | return None, None, None 100 | return l, idnum, s 101 | 102 | def calculate(self): 103 | 104 | if not has_yara: 105 | debug.error('Yara must be installed for this plugin.') 106 | 107 | addr_space = utils.load_as(self._config) 108 | 109 | os, memory_model = self.is_valid_profile(addr_space.profile) 110 | if not os: 111 | debug.error('This command does not support the selected profile.') 112 | 113 | rules = yara.compile(sources=xxmm_sig) 114 | 115 | for task in self.filter_tasks(tasks.pslist(addr_space)): 116 | scanner = malfind.VadYaraScanner(task=task, rules=rules) 117 | for hit, address in scanner.scan(): 118 | 119 | vad_base_addr, end = self.get_vad_base(task, address) 120 | proc_addr_space = task.get_process_address_space() 121 | memdata = proc_addr_space.get_available_addresses() 122 | 123 | config_data = [] 124 | 125 | for m in memdata: 126 | if 0x2000 < m[1]: 127 | continue 128 | p_data = {} 129 | 130 | data = proc_addr_space.zread(m[0], m[1]) 131 | offset = 0 132 | p_data['param'] = [] 133 | while(True): 134 | param = {} 135 | l, param['id'], param['data'] = self.extract_param(data, offset) 136 | if l == None: 137 | if len(p_data['param']) == 1 and p_data['param'][0]['type'] == 'Unknown': 138 | offset = 0 139 | break 140 | for c in data[offset:]: 141 | if ord(c) != 0x00: 142 | offset = 0 143 | break 144 | break 145 | offset += l 146 | if param['id'] in DATA_TYPE.keys(): 147 | param['type'] = DATA_TYPE[param['id']] 148 | else: 149 | param['type'] = 'Unknown' 150 | p_data['param'].append(param) 151 | if offset == 0: 152 | continue 153 | p_data['offset'] = m[0] 154 | p_data['length'] = offset 155 | config_data.append(p_data) 156 | yield task, vad_base_addr, end, hit, memory_model, config_data 157 | break 158 | 159 | def render_text(self, outfd, data): 160 | 161 | delim = '-' * 70 162 | 163 | for task, start, end, malname, memory_model, config_data in data: 164 | self.table_row(outfd, task.ImageFileName, task.UniqueProcessId, start) 165 | outfd.write("{0}\n".format(delim)) 166 | outfd.write("Process: {0} ({1})\n\n".format(task.ImageFileName, task.UniqueProcessId)) 167 | 168 | for p_data in config_data: 169 | outfd.write(' Offset: %8Xh\n' % p_data['offset']) 170 | outfd.write(' Length: %8Xh\n' % p_data['length']) 171 | for param in p_data['param']: 172 | outfd.write(' ID:%6Xh Data(%s): ' % (param['id'], param['type'])) 173 | if param['type'] in {'ASCII', 'UTF-16LE'}: 174 | outfd.write('%s\n' % param['data']) 175 | elif param['type'] == 'DWORD': 176 | outfd.write('%d\n' % unpack('>I', param['data'])[0]) 177 | elif param['type'] in {'BYTE', 'Unknown'}: 178 | for c in param['data']: 179 | outfd.write('%X ' % ord(c)) 180 | outfd.write('\n') 181 | else: 182 | debug.error('Invalid type found.') 183 | outfd.write('%s\n' % ('-' * 10)) 184 | -------------------------------------------------------------------------------- /utils/netwirescan.py: -------------------------------------------------------------------------------- 1 | # NetWire config dumper for Volatility 2 | # 3 | # LICENSE 4 | # Please refer to the LICENSE.txt in the https://github.com/JPCERTCC/MalConfScan/ 5 | # 6 | # Created by You Nakatsuru (@you0708) 7 | # 8 | # How to use: 9 | # 1. cd "Volatility Folder" 10 | # 2. mkdir contrib/plugins/malware 11 | # 3. mv netwirescan.py contrib/plugins/malware 12 | # 4. python vol.py --plugins=contrib/plugins/malware netwireconfig -f images.mem --profile=WinXPSP3x86 13 | 14 | import volatility.plugins.taskmods as taskmods 15 | import volatility.win32.tasks as tasks 16 | import volatility.utils as utils 17 | import volatility.debug as debug 18 | import volatility.plugins.malware.malfind as malfind 19 | from struct import unpack 20 | import re 21 | from collections import OrderedDict 22 | 23 | NETWIRE_INFO = [{ 24 | 'version': '1.5b', 25 | 'pattern': re.compile("\xE8\x8B(.)\x00\x00\xC7\x44(.)\x08\x03\x00\x00\x00", re.DOTALL), 26 | 'cfg_offset': 17, 27 | 'cfg_size': 0x3A4, 28 | 'cfg_info': [['Unknown0', 0], ['Unknown1', 0x4], ['KeyLog Dir', 0x8], ['Active Setup', 0x8C], ['Run Key', 0xB4], ['Startup', 0xC8], ['Mutex', 0x14C], ['UUID', 0x158], 29 | ['Password', 0x180], ['Unknown2', 0x1A4], ['Host', 0x2A4]] 30 | }, { 31 | 'version': '1.5d', 32 | 'pattern': re.compile("\xE8(..)\x00\x00\x89\x1C(.)\xC7\x44\x24\x08", re.DOTALL), 33 | 'cfg_offset': 20, 34 | 'cfg_size': 0x3A8, 35 | 'cfg_info': [['Unknown0', 0], ['Unknown1', 0x4], ['Unknown2', 0x8], ['KeyLog Dir', 0xC], ['Active Setup', 0x90], ['Run Key', 0xB8], ['Startup', 0xCC], ['Mutex', 0x150], ['UUID', 0x15C], 36 | ['Password', 0x184], ['Unknown3', 0x1A8], ['Host', 0x2A8]] 37 | }, { 38 | 'version': '1.6a Final?', 39 | 'pattern': re.compile("\xE8\x87(.)\x00\x00\x89\x1C(.)\xC7\x44\x24\x08\x03", re.DOTALL), 40 | 'cfg_offset': 20, 41 | 'cfg_size': 0x3A8, 42 | 'cfg_info': [['Unknown0', 0], ['Unknown1', 0x4], ['Unknown2', 0x8], ['KeyLog Dir', 0xC], ['Active Setup', 0x90], ['Run Key', 0xB8], ['Startup', 0xCC], ['Mutex', 0x150], ['UUID', 0x15C], 43 | ['Password', 0x184], ['Unknown3', 0x1A8], ['Host', 0x2A8]] 44 | }, { 45 | 'version': '1.6a', 46 | 'pattern': re.compile("\xE8\x9F(.)\x00\x00\x89\x1C(.)\xC7\x44\x24\x08\x03", re.DOTALL), 47 | 'cfg_offset': 20, 48 | 'cfg_size': 0x3A8, 49 | 'cfg_info': [['Unknown0', 0], ['Unknown1', 0x4], ['Unknown2', 0x8], ['KeyLog Dir', 0xC], ['Active Setup', 0x90], ['Run Key', 0xB8], ['Startup', 0xCC], ['Mutex', 0x150], ['UUID', 0x15C], 50 | ['Password', 0x184], ['Unknown3', 0x1A8], ['Host', 0x2A8]] 51 | }, { 52 | 'version': '1.7a', 53 | 'pattern': re.compile("\xE8(..)\x00\x00\xC7\x44(.)\x08\xFF\x00\x00\x00", re.DOTALL), 54 | 'cfg_offset': 17, 55 | 'cfg_size': 0x3D0, 56 | 'cfg_info': [['C2', 0], ['Unknown0', 0x100], ['AES Key', 0x200], ['Host ID', 0x238], ['Mutex', 0x24C], ['Install Path', 0x260], ['Startup', 0x2E4], ['UUID', 0x300], 57 | ['KeyLog Dir', 0x340], ['Unknown1', 0x3C4], ['Unknown2', 0x3C8], ['Unknown3', 0x3CC]] 58 | }, { 59 | 'version': 'Unknown', 60 | 'pattern': re.compile("\xE8\x53(.)\x00\x00\xC7\x44(.)\x10\xFF\x00\x00\x00", re.DOTALL), 61 | 'cfg_offset': 17, 62 | 'cfg_size': 0x468, 63 | 'cfg_info': [['C2', 0], ['Unknown0', 0x100], ['AES Key', 0x200], ['Host ID', 0x238], ['Group', 0x24C], ['Mutex', 0x260], ['Install Path', 0x280], ['Startup', 0x320], ['UUID', 0x360], 64 | ['KeyLog Dir', 0x3A0], ['Unknown1', 0x424], ['Unknown2', 0x440], ['Unknown3', 0x464]] 65 | }] 66 | 67 | try: 68 | import yara 69 | has_yara = True 70 | except ImportError: 71 | has_yara = False 72 | 73 | signatures = { 74 | 'namespace1' : 'rule netwire { \ 75 | strings: \ 76 | $v1 = "HostId-%Rand%" \ 77 | $v2 = "mozsqlite3" \ 78 | $v3 = "[Scroll Lock]" \ 79 | $v4 = "GetRawInputData" \ 80 | $ping = "ping 192.0.2.2" \ 81 | $log = "[Log Started] - [%.2d/%.2d/%d %.2d:%.2d:%.2d]" \ 82 | condition: ($v1) or ($v2 and $v3 and $v4) or ($ping and $log)}' 83 | } 84 | 85 | 86 | class netwireConfig(taskmods.DllList): 87 | """Parse the NetWire configuration""" 88 | 89 | @staticmethod 90 | def is_valid_profile(profile): 91 | return (profile.metadata.get('os', 'unknown') == 'windows'), profile.metadata.get('memory_model', '32bit') 92 | 93 | def get_vad_base(self, task, address): 94 | """ Get the VAD starting address """ 95 | 96 | for vad in task.VadRoot.traverse(): 97 | if address >= vad.Start and address < vad.End: 98 | return vad.Start, vad.End 99 | 100 | # This should never really happen 101 | return None 102 | 103 | def parse_config(self, cfg_blob, nw): 104 | p_data = OrderedDict() 105 | p_data["Version"] = nw["version"] 106 | 107 | for name, offset in nw["cfg_info"]: 108 | data = cfg_blob[offset:].split("\x00")[0] 109 | p_data[name] = data 110 | 111 | return p_data 112 | 113 | def calculate(self): 114 | 115 | if not has_yara: 116 | debug.error("Yara must be installed for this plugin") 117 | 118 | addr_space = utils.load_as(self._config) 119 | 120 | os, memory_model = self.is_valid_profile(addr_space.profile) 121 | if not os: 122 | debug.error("This command does not support the selected profile.") 123 | 124 | rules = yara.compile(sources=signatures) 125 | 126 | for task in self.filter_tasks(tasks.pslist(addr_space)): 127 | scanner = malfind.VadYaraScanner(task=task, rules=rules) 128 | 129 | for hit, address in scanner.scan(): 130 | 131 | vad_base_addr, end = self.get_vad_base(task, address) 132 | proc_addr_space = task.get_process_address_space() 133 | data = proc_addr_space.zread(vad_base_addr, end - vad_base_addr) 134 | 135 | config_data = [] 136 | 137 | if len(data) < 0x10000 or len(data) > 0x200000: 138 | continue 139 | 140 | for nw in NETWIRE_INFO: 141 | m = re.search(nw["pattern"], data) 142 | if m: 143 | offset = m.start() 144 | break 145 | else: 146 | continue 147 | 148 | cfg_addr = unpack("=I", data[offset + nw["cfg_offset"]:offset + nw["cfg_offset"] + 4])[0] 149 | if cfg_addr < vad_base_addr: 150 | continue 151 | 152 | cfg_addr -= vad_base_addr 153 | cfg_blob = data[cfg_addr:cfg_addr + nw["cfg_size"]] 154 | config_data.append(self.parse_config(cfg_blob, nw)) 155 | 156 | yield task, vad_base_addr, end, hit, memory_model, config_data 157 | break 158 | 159 | def render_text(self, outfd, data): 160 | 161 | delim = '-' * 70 162 | 163 | for task, start, end, malname, memory_model, config_data in data: 164 | outfd.write("{0}\n".format(delim)) 165 | outfd.write("Process: {0} ({1})\n\n".format(task.ImageFileName, task.UniqueProcessId)) 166 | 167 | outfd.write("[Config Info]\n") 168 | for p_data in config_data: 169 | for id, param in p_data.items(): 170 | outfd.write("{0:<16}: {1}\n".format(id, param)) 171 | -------------------------------------------------------------------------------- /utils/nanocorescan.py: -------------------------------------------------------------------------------- 1 | # Detecting Nanocore RAT for Volatilitv 2 | # 3 | # Based on the script below: 4 | # https://github.com/kevthehermit/RATDecoders/blob/master/decoders/NanoCore.py 5 | # 6 | # How to use: 7 | # 1. cd "Volatility Folder" 8 | # 2. mv nanocoreconfigallocate.py volatility/plugins/malware 9 | # 3. python vol.py nanocoreconfig -f images.mem --profile=Win7SP1x64 10 | 11 | import volatility.plugins.taskmods as taskmods 12 | import volatility.win32.tasks as tasks 13 | import volatility.utils as utils 14 | import volatility.debug as debug 15 | import volatility.plugins.malware.malfind as malfind 16 | import re 17 | from struct import unpack, unpack_from 18 | from collections import OrderedDict 19 | 20 | try: 21 | import yara 22 | has_yara = True 23 | except ImportError: 24 | has_yara = False 25 | 26 | nanocore_sig = { 27 | 'namespace1' : 'rule Nanocore { \ 28 | strings: \ 29 | $v1 = "NanoCore Client" \ 30 | $v2 = "PluginCommand" \ 31 | $v3 = "CommandType" \ 32 | condition: all of them}' 33 | } 34 | 35 | # Config pattern 36 | CONFIG_PATTERNS = [re.compile("Version.\x07(.*?)\x0cMutex", re.DOTALL)] 37 | 38 | MODE = {0x0: "Disable", 0x01: "Enable"} 39 | 40 | 41 | class nanocoreConfig(taskmods.DllList): 42 | "Parse the Nanocore configuration" 43 | 44 | @staticmethod 45 | def is_valid_profile(profile): 46 | return (profile.metadata.get('os', 'unknown') == 'windows'), profile.metadata.get('memory_model', '32bit') 47 | 48 | def get_vad_base(self, task, address): 49 | for vad in task.VadRoot.traverse(): 50 | if address >= vad.Start and address < vad.End: 51 | return vad.Start, vad.End 52 | return None 53 | 54 | def parse_config(self, data): 55 | 56 | p_data = OrderedDict() 57 | 58 | p_data['Version'] = re.search('Version..(.*?)\x0c', data).group()[8:16] 59 | p_data['Mutex'] = re.search('Mutex(.*?)\x0c', data).group()[6:-1].encode('hex') 60 | p_data['Group'] = re.search('DefaultGroup\x0c(.*?)\x0c', data).group()[14:-1] 61 | p_data['Domain1'] = re.search('PrimaryConnectionHost\x0c(.*?)Back', data, re.DOTALL).group()[23:-6] 62 | p_data['Domain2'] = re.search('BackupConnectionHost\x0c(.*?)\x0c', data).group()[22:-1] 63 | p_data['Port'] = unpack("= vad.Start and address < vad.End: 53 | return vad.Start, vad.End 54 | 55 | return None 56 | 57 | def crc32(self, buf, value): 58 | table = [] 59 | 60 | for i in range(256): 61 | v = i 62 | for j in range(8): 63 | v = (0xEDB88320 ^ (v >> 1)) if(v & 1) == 1 else (v >> 1) 64 | table.append(v) 65 | 66 | for c in buf: 67 | 68 | value = value ^ ord(c) 69 | value = table[value & 0xFF] ^ (value >> 8) 70 | 71 | return value 72 | 73 | def sum_of_characters(self, domain): 74 | return sum([ord(d) for d in domain[:-3]]) 75 | 76 | def get_next_domain(self, domain, xor): 77 | qwerty = "qwertyuiopasdfghjklzxcvbnm123945678" 78 | 79 | sof = self.sum_of_characters(domain) ^ xor 80 | ascii_codes = [ord(d) for d in domain] + 100 * [0] 81 | old_hostname_length = len(domain) - 4 82 | for i in range(0, 66): 83 | for j in range(0, 66): 84 | edi = j + i 85 | if edi < 65: 86 | p = (old_hostname_length * ascii_codes[j]) 87 | cl = p ^ ascii_codes[edi] ^ sof 88 | ascii_codes[edi] = cl & 0xFF 89 | 90 | """ 91 | calculate the new hostname length 92 | max: 255/16 = 15 93 | min: 10 94 | """ 95 | cx = ((ascii_codes[2] * old_hostname_length) ^ ascii_codes[0]) & 0xFF 96 | hostname_length = int(cx / 16) # at most 15 97 | if hostname_length < 10: 98 | hostname_length = old_hostname_length 99 | 100 | """ 101 | generate hostname 102 | """ 103 | for i in range(hostname_length): 104 | index = int(ascii_codes[i] / 8) # max 31 --> last 3 chars of qwerty unreachable 105 | bl = ord(qwerty[index]) 106 | ascii_codes[i] = bl 107 | 108 | hostname = ''.join([chr(a) for a in ascii_codes[:hostname_length]]) 109 | 110 | """ 111 | append .net or .com (alternating) 112 | """ 113 | tld = '.com' if domain.endswith('.net') else '.net' 114 | domain = hostname + tld 115 | 116 | return domain 117 | 118 | def parse_config(self, data, base, rsa_key, dga_key): 119 | p_data = OrderedDict() 120 | p_data["RSA key"] = rsa_key.encode("hex") 121 | p_data["Sleep count"] = unpack_from("= vad.Start and address < vad.End: 66 | return vad.Start, vad.End 67 | 68 | return None 69 | 70 | def xor(self, encoded, xor_key): 71 | count = 0 72 | decode = [] 73 | key_len = len(xor_key) 74 | for n in range(len(encoded)): 75 | if count == 0: 76 | count = key_len - 1 77 | decode.append(chr(ord(encoded[n]) ^ ord(xor_key[count]))) 78 | count -= 1 79 | 80 | return "".join(decode) 81 | 82 | def parse_config(self, pe, data, base): 83 | p_data = OrderedDict() 84 | p_data["DGA Damain No"] = unpack_from("I", data, base)[0] 85 | p_data["DGA Damain Seed"] = unpack_from(">I", data, base + 4)[0] 86 | p_data["Magick Check"] = FLAG[unpack_from("I", data, base + 8)[0]] 87 | p_data["Magick"] = unpack_from(">I", data, base + 0xc)[0] 88 | p_data["Use IP Address"] = FLAG[unpack_from("I", data, base + 0x10)[0]] 89 | p_data["IP Address"] = inet_ntoa(data[base + 0x14:base + 0x18]) 90 | p_data["Port"] = unpack_from("I", data, base + 0x18)[0] 91 | key_len = unpack_from("I", data, base + 0x1c)[0] 92 | p_data["XOR key length"] = key_len 93 | 94 | for pattern in XOR_KEY_PATTERNS: 95 | mk = re.search(pattern, data) 96 | 97 | if mk: 98 | (resource_name_rva, ) = unpack("=I", data[mk.start() - 4:mk.start()]) 99 | rn_addr = pe.get_physical_by_rva(resource_name_rva - pe.NT_HEADERS.OPTIONAL_HEADER.ImageBase) 100 | xor_key = data[rn_addr:rn_addr + key_len] 101 | else: 102 | xor_key = "" 103 | outfd.write("[!] Not found XOR key.\n") 104 | 105 | domain_encoded_data = data[base + 0x20:base + 0x15c].replace("\0","") 106 | botnet_encoded_data = data[base + 0x15c:base + 0x1ca].replace("\0","") 107 | encoded_data_1 = data[base + 0x1ca:base + 0x240].replace("\0","") 108 | encoded_data_2 = data[base + 0x240:base + 0x2b6].replace("\0","") 109 | rc4_encoded_data = data[base + 0x2b6:base + 0x2f1].replace("\0","") 110 | 111 | p_data["Hardcode Domain"] = self.xor(domain_encoded_data, xor_key) 112 | p_data["Botnet name"] = self.xor(botnet_encoded_data, xor_key) 113 | p_data["Unknown 1"] = self.xor(encoded_data_1, xor_key) 114 | p_data["Unknown 2"] = self.xor(encoded_data_2, xor_key) 115 | p_data["RC4 Key"] = self.xor(rc4_encoded_data, xor_key) 116 | 117 | return p_data 118 | 119 | def calculate(self): 120 | 121 | if not has_yara: 122 | debug.error("Yara must be installed for this plugin") 123 | 124 | addr_space = utils.load_as(self._config) 125 | 126 | os, memory_model = self.is_valid_profile(addr_space.profile) 127 | if not os: 128 | debug.error("This command does not support the selected profile.") 129 | 130 | rules = yara.compile(sources=ramnit_sig) 131 | 132 | for task in self.filter_tasks(tasks.pslist(addr_space)): 133 | scanner = malfind.VadYaraScanner(task=task, rules=rules) 134 | 135 | for hit, address in scanner.scan(): 136 | 137 | vad_base_addr, end = self.get_vad_base(task, address) 138 | proc_addr_space = task.get_process_address_space() 139 | data = proc_addr_space.zread(vad_base_addr, end - vad_base_addr) 140 | 141 | config_data = [] 142 | 143 | # resource PE search 144 | dll_index = data.rfind(MZ_HEADER) 145 | dll_data = data[dll_index:] 146 | 147 | try: 148 | pe = pefile.PE(data=dll_data) 149 | except: 150 | outfd.write("[!] Can't mapped PE.\n") 151 | continue 152 | 153 | for section in pe.sections: 154 | if ".data" in section.Name: 155 | data_address = section.PointerToRawData 156 | 157 | config_data.append(self.parse_config(pe, dll_data, data_address)) 158 | 159 | yield task, vad_base_addr, end, hit, memory_model, config_data 160 | break 161 | 162 | def render_text(self, outfd, data): 163 | 164 | delim = '-' * 70 165 | 166 | for task, start, end, malname, memory_model, config_data in data: 167 | outfd.write("{0}\n".format(delim)) 168 | outfd.write("Process: {0} ({1})\n\n".format(task.ImageFileName, task.UniqueProcessId)) 169 | 170 | outfd.write("[Config Info]\n") 171 | for p_data in config_data: 172 | for id, param in p_data.items(): 173 | outfd.write("{0:<16}: {1}\n".format(id, param)) 174 | -------------------------------------------------------------------------------- /utils/asyncratscan.py: -------------------------------------------------------------------------------- 1 | import volatility.plugins.taskmods as taskmods 2 | import volatility.win32.tasks as tasks 3 | import volatility.utils as utils 4 | import volatility.debug as debug 5 | import volatility.plugins.malware.malfind as malfind 6 | from struct import unpack, pack 7 | from base64 import b64decode 8 | from collections import OrderedDict 9 | 10 | try: 11 | import yara 12 | has_yara = True 13 | except ImportError: 14 | has_yara = False 15 | 16 | try: 17 | from Crypto.Cipher import AES 18 | from Crypto.Protocol.KDF import PBKDF2 19 | has_crypto = True 20 | except ImportError: 21 | has_crypto = False 22 | 23 | asyncrat_sig = { 24 | 'namespace1': 'rule asyncrat { \ 25 | strings: \ 26 | $salt = {BF EB 1E 56 FB CD 97 3B B2 19 02 24 30 A5 78 43 00 3D 56 44 D2 1E 62 B9 D4 F1 80 E7 E6 C3 39 41}\ 27 | $b1 = {00 00 00 0D 53 00 48 00 41 00 32 00 35 00 36 00 00}\ 28 | $b2 = {09 50 00 6F 00 6E 00 67 00 00}\ 29 | $s1 = "pastebin" ascii wide nocase \ 30 | $s2 = "pong" wide\ 31 | $s3 = "Stub.exe" ascii wide\ 32 | condition: ($salt and (2 of ($s*) or 1 of ($b*))) or (all of ($b*) and 2 of ($s*)) }' 33 | } 34 | 35 | CONFIG_PATTERNS = [ 36 | b"\x00\x00\x00\x0D\x53\x00\x48\x00\x41\x00\x32\x00\x35\x00\x36\x00\x00"] 37 | 38 | ## format "index" : ("position_in_storage_stream","field_name","encryption_method") 39 | config_index = { 40 | 1: (2,"Server", "aes"), 41 | 2: (1,"Ports", "aes"), 42 | 3: (3,"Version", "aes"), 43 | 4: (4,"Autorun", "aes"), 44 | 5: (5,"Install_Folder", ""), 45 | 6: (6,"Install_File", "aes"), 46 | 7: (7,"AES_key", "base64"), 47 | 8: (8,"Mutex", "aes"), 48 | 9: (11,"AntiDetection", "aes"), 49 | 10: (12,"External_config_on_Pastebin", "aes"), 50 | 11: (13,"BDOS", "aes"), 51 | 12: (14,"HWID", ""), 52 | 13: (15,"Startup_Delay", ""), 53 | 14: (9,"Certificate", "aes"), 54 | 15: (10,"ServerSignature", "aes") 55 | } 56 | 57 | 58 | class asyncratConfig(taskmods.DllList): 59 | """Parse the asyncrat configuration""" 60 | 61 | @staticmethod 62 | def is_valid_profile(profile): 63 | return (profile.metadata.get('os', 'unknown') == 'windows'), profile.metadata.get('memory_model', '32bit') 64 | 65 | def get_vad_base(self, task, address): 66 | for vad in task.VadRoot.traverse(): 67 | if address >= vad.Start and address < vad.End: 68 | return vad.Start, vad.End 69 | 70 | return None 71 | 72 | def printable(self, data): 73 | if len(data) < 1: 74 | return data 75 | cleaned = "" 76 | 77 | for d in data: 78 | if 0x20 <= ord(d) and ord(d) <= 0x7F: 79 | cleaned += d 80 | 81 | return cleaned 82 | 83 | def storage_stream_us_parser(self, data): 84 | """ 85 | parse storage_stream for unicode strings in .NET assembly. 86 | unicode_strings chunk patterns 87 | pat1: [size of unicode strings(1byte)][unicode strings][terminate code(0x00 or 0x01)] 88 | pat2: [size of unicode strings(2byte)][unicode strings][terminate code(0x00 or 0x01)] 89 | """ 90 | if len(data) < 2: 91 | return list() 92 | unicode_strings = list() 93 | 94 | while True: 95 | # first byte must be the size of unicode strings chunk. 96 | initial_byte = ord(data[0]) 97 | if initial_byte == 0x00: 98 | break 99 | elif initial_byte < 0x80: 100 | size = initial_byte 101 | p = 1 102 | elif initial_byte >= 0x80: 103 | size = unpack(">H",pack("B",initial_byte-0x80)+data[1])[0] 104 | # size = int.from_bytes(bytes([data[0]-0x80, data[1]]), "big") 105 | p = 2 106 | 107 | if size < 0 or 0x7FFF < size or size > len(data)-3: 108 | debug.info("Invalid string size found in stroage stream.") 109 | break 110 | try: 111 | unicode_strings.append( 112 | data[p:size + p - 1].decode().replace("\x00", "")) 113 | except UnicodeDecodeError: 114 | debug.info("Invalid unicode byte(s) found in storage stream.") 115 | pass 116 | # check the termination code. 117 | termination_byte = ord(data[size + p - 1]) 118 | if termination_byte == 0x00 or termination_byte == 0x01: 119 | # goto next block 120 | data = data[size + p:] 121 | continue 122 | else: 123 | debug.info("Invalid termination code: {}".format(termination_byte)) 124 | break 125 | 126 | return unicode_strings 127 | 128 | def parse_config(self, unicode_strings): 129 | 130 | if len(unicode_strings) < 7: 131 | debug.info("unicode strings list is too short.") 132 | return OrderedDict() 133 | 134 | config = OrderedDict() 135 | 136 | key = b64decode(unicode_strings[7]) 137 | salt = "BFEB1E56FBCD973BB219022430A57843003D5644D21E62B9D4F180E7E6C33941".decode("hex") 138 | aes_key = PBKDF2(key, salt, 32, 50000) 139 | 140 | for _ , params in config_index.items(): 141 | pos, field, enc_type = params 142 | if enc_type == "aes" and len(unicode_strings[pos]) > 48: 143 | enc_data = b64decode(unicode_strings[pos]) 144 | # hmac = enc_data[:32] 145 | aes_iv = enc_data[32:48] 146 | cipher = AES.new(aes_key, AES.MODE_CBC, aes_iv) 147 | value = self.printable(cipher.decrypt(enc_data[48:])) 148 | elif enc_type == "base64": 149 | value = self.printable(b64decode(unicode_strings[pos])) 150 | else: 151 | value = unicode_strings[pos] 152 | config[field] = value 153 | return config 154 | 155 | def calculate(self): 156 | 157 | if not has_yara: 158 | debug.error("Yara must be installed for this plugin") 159 | 160 | if not has_crypto: 161 | debug.error("pycrypto must be installed for this plugin") 162 | 163 | addr_space = utils.load_as(self._config) 164 | 165 | os, memory_model = self.is_valid_profile(addr_space.profile) 166 | if not os: 167 | debug.error("This command does not support the selected profile.") 168 | 169 | rules = yara.compile(sources=asyncrat_sig) 170 | 171 | for task in self.filter_tasks(tasks.pslist(addr_space)): 172 | scanner = malfind.VadYaraScanner(task=task, rules=rules) 173 | 174 | for hit, address in scanner.scan(): 175 | vad_base_addr, end = self.get_vad_base(task, address) 176 | proc_addr_space = task.get_process_address_space() 177 | data = proc_addr_space.zread( 178 | vad_base_addr, end - vad_base_addr) 179 | 180 | config_data = [] 181 | dlist = OrderedDict() 182 | 183 | for pattern in CONFIG_PATTERNS: 184 | m = data.find(pattern) 185 | if m > 0: 186 | unicode_strings = self.storage_stream_us_parser( 187 | data[m + 3:]) 188 | dlist = self.parse_config(unicode_strings) 189 | break 190 | else: 191 | debug.info( 192 | "Asyncrat configuration signature not found.") 193 | 194 | config_data.append(dlist) 195 | 196 | yield task, vad_base_addr, end, hit, memory_model, config_data 197 | break 198 | 199 | def render_text(self, outfd, data): 200 | 201 | delim = '-' * 70 202 | 203 | for task, start, end, malname, memory_model, config_data in data: 204 | outfd.write("{0}\n".format(delim)) 205 | outfd.write("Process: {0} ({1})\n\n".format( 206 | task.ImageFileName, task.UniqueProcessId)) 207 | 208 | outfd.write("[Config Info]\n") 209 | for p_data in config_data: 210 | for id, param in p_data.items(): 211 | outfd.write("{0:<25}: {1}\n".format(id, param)) 212 | -------------------------------------------------------------------------------- /utils/quasarscan.py: -------------------------------------------------------------------------------- 1 | # Detecting QuasarRAT for Volatility 2 | # 3 | # LICENSE 4 | # Please refer to the LICENSE.txt in the https://github.com/JPCERTCC/MalConfScan/ 5 | # 6 | # How to use: 7 | # 1. cd "Volatility Folder" 8 | # 2. mv quasarscan.py volatility/plugins/malware 9 | # 3. python vol.py quasarconfig -f images.mem --profile=Win7SP1x64 10 | 11 | import volatility.plugins.taskmods as taskmods 12 | import volatility.win32.tasks as tasks 13 | import volatility.utils as utils 14 | import volatility.debug as debug 15 | import volatility.plugins.malware.malfind as malfind 16 | import re 17 | import hashlib 18 | from base64 import b64decode 19 | from collections import OrderedDict 20 | 21 | try: 22 | import yara 23 | has_yara = True 24 | except ImportError: 25 | has_yara = False 26 | 27 | try: 28 | from Crypto.Cipher import AES 29 | has_crypto = True 30 | except ImportError: 31 | has_crypto = False 32 | 33 | try: 34 | from pbkdf2 import PBKDF2 35 | has_pbkdf2 = True 36 | except ImportError: 37 | has_pbkdf2 = False 38 | 39 | quasar_sig = { 40 | 'namespace1' : 'rule Quasar { \ 41 | strings: \ 42 | $quasarstr1 = "Client.exe" wide \ 43 | $quasarstr2 = "({0}:{1}:{2})" wide \ 44 | $sql1 = "SELECT * FROM Win32_DisplayConfiguration" wide \ 45 | $sql2 = "{0}d : {1}h : {2}m : {3}s" wide \ 46 | $sql3 = "SELECT * FROM FirewallProduct" wide \ 47 | $net1 = "echo DONT CLOSE THIS WINDOW!" wide \ 48 | $net2 = "freegeoip.net/xml/" wide \ 49 | $net3 = "http://api.ipify.org/" wide \ 50 | $resource = { 52 00 65 00 73 00 6F 00 75 00 72 00 63 00 65 00 73 00 00 17 69 00 6E 00 66 00 6F 00 72 00 6D 00 61 00 74 00 69 00 6F 00 6E 00 00 } \ 51 | condition: ((all of ($quasarstr*) or all of ($sql*)) and $resource) or all of ($net*)}' 52 | } 53 | 54 | # Config pattern 55 | CONFIG_PATTERNS = [re.compile("\x52\x00\x65\x00\x73\x00\x6F\x00\x75\x00\x72\x00\x63\x00\x65\x00\x73\x00\x00\x17\x69\x00\x6E\x00\x66\x00\x6F\x00\x72\x00\x6D\x00\x61\x00\x74\x00\x69\x00\x6F\x00\x6E\x00\x00\x80", re.DOTALL), 56 | re.compile("\x61\x00\x70\x00\x69\x00\x2E\x00\x69\x00\x70\x00\x69\x00\x66\x00\x79\x00\x2E\x00\x6F\x00\x72\x00\x67\x00\x2F\x00\x00\x03\x5C\x00\x00", re.DOTALL), 57 | re.compile("\x3C\x00\x2F\x00\x73\x00\x74\x00\x79\x00\x6C\x00\x65\x00\x3E\x00\x00\x03\x5C\x00\x00\x80", re.DOTALL)] 58 | 59 | idx_list = { 60 | 0: ["VERSION", True], 61 | 1: ["HOSTS", True], 62 | 2: ["KEY (Base64)", False], 63 | 3: ["AUTHKEY (Base64)", False], 64 | 4: ["SUBDIRECTORY", True], 65 | 5: ["INSTALLNAME",True], 66 | 6: ["MUTEX", True], 67 | 7: ["STARTUPKEY", True], 68 | 8: ["ENCRYPTIONKEY", False], 69 | 9: ["TAG", True], 70 | 10: ["LOGDIRECTORYNAME",True ], 71 | 11: ["unknown1", True], 72 | 12: ["unknown2", True] 73 | } 74 | 75 | idx_list_2 = { 76 | 0: ["VERSION", True], 77 | 1: ["HOSTS", True], 78 | 2: ["KEY (Base64)", False], 79 | 3: ["SUBDIRECTORY", True], 80 | 4: ["INSTALLNAME",True], 81 | 5: ["MUTEX", True], 82 | 6: ["STARTUPKEY", True], 83 | 7: ["ENCRYPTIONKEY", False], 84 | 8: ["TAG", True] 85 | } 86 | 87 | 88 | class quasarConfig(taskmods.DllList): 89 | """Parse the QuasarRAT configuration""" 90 | 91 | @staticmethod 92 | def is_valid_profile(profile): 93 | return (profile.metadata.get('os', 'unknown') == 'windows'), profile.metadata.get('memory_model', '32bit') 94 | 95 | def get_vad_base(self, task, address): 96 | for vad in task.VadRoot.traverse(): 97 | if address >= vad.Start and address < vad.End: 98 | return vad.Start, vad.End 99 | 100 | return None 101 | 102 | def decrypt_string(self, key, configs, mode, idx): 103 | p_data = OrderedDict() 104 | for i, config in enumerate(configs): 105 | if idx[i][1] == True: 106 | if len(configs) < 10: 107 | config = b64decode(config) 108 | aes_iv = config[:16] 109 | cipher = AES.new(key, mode, IV=aes_iv) 110 | value = re.sub("[\x00-\x19]" ,"" , cipher.decrypt(config[16:])) 111 | else: 112 | config = b64decode(config) 113 | aes_iv = config[32:48] 114 | cipher = AES.new(key, mode, IV=aes_iv) 115 | value = re.sub("[\x00-\x19]" ,"" , cipher.decrypt(config[48:])) 116 | else: 117 | value = config 118 | p_data[idx[i][0]] = value 119 | 120 | return p_data 121 | 122 | def parse_config(self, configs): 123 | if len(configs) > 10: 124 | idx = idx_list 125 | key, salt = configs[8], 'BFEB1E56FBCD973BB219022430A57843003D5644D21E62B9D4F180E7E6C33941'.decode('hex') 126 | 127 | generator = PBKDF2(key, salt, 50000) 128 | aes_key = generator.read(16) 129 | else: 130 | idx = idx_list_2 131 | aes_key = hashlib.md5(configs[7]).digest() 132 | 133 | if(len(configs) > 12): 134 | mode = AES.MODE_CFB 135 | else: 136 | mode = AES.MODE_CBC 137 | p_data = self.decrypt_string(aes_key, configs, mode, idx) 138 | 139 | return p_data 140 | 141 | def calculate(self): 142 | 143 | if not has_yara: 144 | debug.error("Yara must be installed for this plugin") 145 | 146 | if not has_crypto: 147 | debug.error("pycrypto must be installed for this plugin") 148 | 149 | if not has_pbkdf2: 150 | debug.error("pbkdf2 must be installed for this plugin") 151 | 152 | addr_space = utils.load_as(self._config) 153 | 154 | os, memory_model = self.is_valid_profile(addr_space.profile) 155 | if not os: 156 | debug.error("This command does not support the selected profile.") 157 | 158 | rules = yara.compile(sources=quasar_sig) 159 | 160 | for task in self.filter_tasks(tasks.pslist(addr_space)): 161 | scanner = malfind.VadYaraScanner(task=task, rules=rules) 162 | 163 | for hit, address in scanner.scan(): 164 | 165 | vad_base_addr, end = self.get_vad_base(task, address) 166 | proc_addr_space = task.get_process_address_space() 167 | data = proc_addr_space.zread(vad_base_addr, end - vad_base_addr) 168 | 169 | config_data = [] 170 | 171 | offset = 0 172 | for pattern in CONFIG_PATTERNS: 173 | mc = re.search(pattern, data) 174 | if mc: 175 | offset = mc.end() 176 | 177 | if ord(data[offset]) == 0x0: 178 | offset += 1 179 | 180 | configs = [] 181 | if offset > 0: 182 | while 1: 183 | strings = [] 184 | string_len = ord(data[offset]) 185 | if ord(data[offset]) == 0x80 or ord(data[offset]) == 0x81: 186 | string_len = ord(data[offset + 1]) + ((ord(data[offset]) - 0x80) * 256) 187 | offset += 1 188 | 189 | offset += 1 190 | for i in range(string_len): 191 | if data[offset + i] != "\x00": 192 | strings.append(data[offset + i]) 193 | configs.append("".join(strings)) 194 | offset = offset + string_len 195 | 196 | if ord(data[offset]) < 0x20: 197 | break 198 | 199 | config_data.append(self.parse_config(configs)) 200 | 201 | yield task, vad_base_addr, end, hit, memory_model, config_data 202 | break 203 | 204 | def render_text(self, outfd, data): 205 | 206 | delim = '-' * 70 207 | 208 | for task, start, end, malname, memory_model, config_data in data: 209 | outfd.write("{0}\n".format(delim)) 210 | outfd.write("Process: {0} ({1})\n\n".format(task.ImageFileName, task.UniqueProcessId)) 211 | 212 | outfd.write("[Config Info]\n") 213 | for p_data in config_data: 214 | for id, param in p_data.items(): 215 | outfd.write("{0:<21}: {1}\n".format(id, param)) 216 | -------------------------------------------------------------------------------- /utils/remcosscan.py: -------------------------------------------------------------------------------- 1 | # Detecting Remcos for Volatility 2 | # 3 | # LICENSE 4 | # Please refer to the LICENSE.txt in the https://github.com/JPCERTCC/MalConfScan/ 5 | # 6 | # How to use: 7 | # 1. cd "Volatility Folder" 8 | # 2. mv remcosscan.py volatility/plugins/malware 9 | # 3. python vol.py remcosconfig -f images.mem --profile=Win7SP1x64 10 | 11 | import volatility.plugins.taskmods as taskmods 12 | import volatility.win32.tasks as tasks 13 | import volatility.utils as utils 14 | import volatility.debug as debug 15 | import volatility.plugins.malware.malfind as malfind 16 | import re 17 | import pefile 18 | from struct import unpack, unpack_from 19 | from socket import inet_ntoa 20 | from collections import OrderedDict 21 | 22 | try: 23 | import yara 24 | has_yara = True 25 | except ImportError: 26 | has_yara = False 27 | 28 | remcos_sig = { 29 | 'namespace1' : 'rule Remcos { \ 30 | strings: \ 31 | $remcos = "Remcos" ascii fullword \ 32 | $url1 = "Breaking-Security.Net" ascii fullword \ 33 | $url2 = "BreakingSecurity.Net" ascii fullword \ 34 | $resource = "SETTINGS" ascii wide fullword \ 35 | condition: 1 of ($url*) and $remcos and $resource}' 36 | } 37 | 38 | # MZ Header 39 | MZ_HEADER = b"\x4D\x5A\x90\x00" 40 | 41 | # Resource pattern 42 | RESOURCE_PATTERNS = [re.compile("\xE0\x00\x00\x07\xE0\x00\x00\x07\xFF\xFF\xFF\xFF", re.DOTALL)] 43 | 44 | # Flag 45 | FLAG = {"\x00": "Disable", "\x01": "Enable"} 46 | 47 | idx_list = { 48 | 0: "Host:Port:Password", 49 | 1: "Assigned name", 50 | 2: "Connect interval", 51 | 3: "Install flag", 52 | 4: "Setup HKCU\\Run", 53 | 5: "Setup HKLM\\Run", 54 | 6: "Setup HKLM\\Explorer\\Run", 55 | 7: "Setup HKLM\\Winlogon\\Shell", 56 | 8: "Setup HKLM\\Winlogon\\Userinit", 57 | 9: "Install path", 58 | 10: "Copy file", 59 | 11: "Startup value", 60 | 12: "Hide file", 61 | 13: "Unknown13", 62 | 14: "Mutex", 63 | 15: "Keylog flag", 64 | 16: "Keylog path", 65 | 17: "Keylog file", 66 | 18: "Keylog crypt", 67 | 19: "Hide keylog file", 68 | 20: "Screenshot flag", 69 | 21: "Screenshot time", 70 | 22: "Take Screenshot option", 71 | 23: "Take screenshot title", 72 | 24: "Take screenshot time", 73 | 25: "Screenshot path", 74 | 26: "Screenshot file", 75 | 27: "Screenshot crypt", 76 | 28: "Mouse option", 77 | 29: "Unknown29", 78 | 30: "Delete file", 79 | 31: "Unknown31", 80 | 32: "Unknown32", 81 | 33: "Unknown33", 82 | 34: "Unknown34", 83 | 35: "Unknown35", 84 | 36: "Audio record time", 85 | 37: "Audio path", 86 | 38: "Audio folder", 87 | 39: "Unknown39", 88 | 40: "Unknown40", 89 | 41: "Connect delay", 90 | 42: "Unknown42", 91 | 43: "Unknown43", 92 | 44: "Unknown44", 93 | 45: "Unknown45", 94 | 46: "Unknown46", 95 | 47: "Unknown47", 96 | 48: "Copy folder", 97 | 49: "Keylog folder", 98 | 50: "Unknown50", 99 | 51: "Unknown51", 100 | 52: "Unknown52", 101 | 53: "Unknown53", 102 | 54: "Keylog file max size", 103 | 55: "Unknown55", 104 | } 105 | 106 | setup_list = { 107 | 0: "Temp", 108 | 2: "Root", 109 | 3: "Windows", 110 | 4: "System32", 111 | 5: "Program Files", 112 | 6: "AppData", 113 | 7: "User Profile", 114 | 8: "Application path", 115 | } 116 | 117 | class remcosConfig(taskmods.DllList): 118 | """Parse the Remcos configuration""" 119 | 120 | @staticmethod 121 | def is_valid_profile(profile): 122 | return (profile.metadata.get('os', 'unknown') == 'windows'), profile.metadata.get('memory_model', '32bit') 123 | 124 | def get_vad_base(self, task, address): 125 | for vad in task.VadRoot.traverse(): 126 | if address >= vad.Start and address < vad.End: 127 | return vad.Start, vad.End 128 | 129 | return None 130 | 131 | # RC4 132 | def rc4(self, data, key): 133 | x = 0 134 | box = range(256) 135 | for i in range(256): 136 | x = (x + box[i] + ord(key[i % len(key)])) % 256 137 | box[i], box[x] = box[x], box[i] 138 | x = 0 139 | y = 0 140 | out = [] 141 | for char in data: 142 | x = (x + 1) % 256 143 | y = (y + box[x]) % 256 144 | box[x], box[y] = box[y], box[x] 145 | out.append(chr(ord(char) ^ box[(box[x] + box[y]) % 256])) 146 | 147 | return ''.join(out) 148 | 149 | def parse_config(self, data): 150 | p_data = OrderedDict() 151 | 152 | key_len = ord(data[0]) 153 | key = data[1:key_len + 1] 154 | enc_data = data[key_len + 1:] 155 | config = self.rc4(enc_data, key) 156 | 157 | #configs = config.split("@@") 158 | configs = re.split("\x1E|@@", config) 159 | 160 | for i, cont in enumerate(configs): 161 | if cont == "\x00" or cont == "\x01": 162 | p_data[idx_list[i]] = FLAG[cont] 163 | else: 164 | if i in [9, 16, 25, 37]: 165 | p_data[idx_list[i]] = setup_list[int(cont)] 166 | else: 167 | p_data[idx_list[i]] = cont 168 | 169 | return p_data 170 | 171 | def calculate(self): 172 | 173 | if not has_yara: 174 | debug.error("Yara must be installed for this plugin") 175 | 176 | addr_space = utils.load_as(self._config) 177 | 178 | os, memory_model = self.is_valid_profile(addr_space.profile) 179 | if not os: 180 | debug.error("This command does not support the selected profile.") 181 | 182 | rules = yara.compile(sources=remcos_sig) 183 | 184 | for task in self.filter_tasks(tasks.pslist(addr_space)): 185 | scanner = malfind.VadYaraScanner(task=task, rules=rules) 186 | 187 | for hit, address in scanner.scan(): 188 | 189 | vad_base_addr, end = self.get_vad_base(task, address) 190 | proc_addr_space = task.get_process_address_space() 191 | data = proc_addr_space.zread(vad_base_addr, end - vad_base_addr) 192 | 193 | config_data = [] 194 | 195 | # resource PE search 196 | dll_index = data.rfind(MZ_HEADER) 197 | dll_data = data[dll_index:] 198 | 199 | try: 200 | pe = pefile.PE(data=dll_data) 201 | except: 202 | outfd.write("[!] Can't mapped PE.\n") 203 | continue 204 | 205 | rc_data = "" 206 | for idx in pe.DIRECTORY_ENTRY_RESOURCE.entries: 207 | for entry in idx.directory.entries: 208 | if str(entry.name) in "SETTINGS": 209 | try: 210 | data_rva = entry.directory.entries[0].data.struct.OffsetToData 211 | size = entry.directory.entries[0].data.struct.Size 212 | rc_data = dll_data[data_rva:data_rva + size] 213 | print("[*] Found SETTINGS resource.") 214 | except: 215 | debug.error("Faild to load SETTINGS resource.") 216 | 217 | if not len(rc_data): 218 | for pattern in RESOURCE_PATTERNS: 219 | mc = re.search(pattern, dll_data) 220 | if mc: 221 | try: 222 | config_end = mc.end() + 1 223 | while dll_data[config_end:config_end + 2] != "\x00\x00": 224 | config_end += 1 225 | rc_data = dll_data[mc.end():config_end - 1] 226 | except: 227 | debug.error("Remcos resource not found.") 228 | 229 | config_data.append(self.parse_config(rc_data)) 230 | 231 | yield task, vad_base_addr, end, hit, memory_model, config_data 232 | break 233 | 234 | def render_text(self, outfd, data): 235 | 236 | delim = '-' * 70 237 | 238 | for task, start, end, malname, memory_model, config_data in data: 239 | outfd.write("{0}\n".format(delim)) 240 | outfd.write("Process: {0} ({1})\n\n".format(task.ImageFileName, task.UniqueProcessId)) 241 | 242 | outfd.write("[Config Info]\n") 243 | for p_data in config_data: 244 | for id, param in p_data.items(): 245 | outfd.write("{0:<16}: {1}\n".format(id, param)) 246 | -------------------------------------------------------------------------------- /utils/cobaltstrikescan.py: -------------------------------------------------------------------------------- 1 | # Detecting CobaltStrike for Volatility 2 | # 3 | # LICENSE 4 | # Please refer to the LICENSE.txt in the https://github.com/JPCERTCC/MalConfScan/ 5 | # 6 | # How to use: 7 | # 1. locate "cobaltstrikescan.py" in [Volatility_Plugins_Directory] 8 | # ex) mv cobaltstrikescan.py /usr/lib/python2.7/dist-packages/volatility/plugins/malware 9 | # 2. python vol.py cobaltstrikeconfig -f images.mem --profile=Win7SP1x64 10 | 11 | import volatility.plugins.taskmods as taskmods 12 | import volatility.win32.tasks as tasks 13 | import volatility.utils as utils 14 | import volatility.debug as debug 15 | import volatility.plugins.malware.malfind as malfind 16 | import re 17 | from struct import unpack, unpack_from 18 | from socket import inet_ntoa 19 | from collections import OrderedDict 20 | 21 | try: 22 | import yara 23 | has_yara = True 24 | except ImportError: 25 | has_yara = False 26 | 27 | cobaltstrike_sig = { 28 | 'namespace1' : 'rule CobaltStrike { \ 29 | strings: \ 30 | $v1 = { 73 70 72 6E 67 00} \ 31 | $v2 = { 69 69 69 69 69 69 69 69} \ 32 | condition: $v1 and $v2}' 33 | } 34 | 35 | CONF_PATTERNS = [{ 36 | 'pattern': '\x69\x68\x69\x68\x69', 37 | 'cfg_size': 0x1000, 38 | 'cfg_info': [['\x00\x01\x00\x01\x00\x02', 'BeaconType', 0x2], ['\x00\x02\x00\x01\x00\x02', 'Port', 0x2], ['\x00\x03\x00\x02\x00\x04', 'Polling(ms)', 0x4], 39 | ['\x00\x04\x00\x02\x00\x04', 'Unknown1', 0x4], ['\x00\x05\x00\x01\x00\x02', 'Jitter', 0x2], ['\x00\x06\x00\x01\x00\x02', 'Maxdns', 0x2], 40 | ['\x00\x07\x00\x03\x01\x00', 'Unknown2', 0x100], ['\x00\x08\x00\x03\x01\x00', 'C2Server', 0x100], ['\x00\x09\x00\x03\x00\x80', 'UserAgent', 0x80], 41 | ['\x00\x0a\x00\x03\x00\x40', 'HTTP_Method2_Path', 0x40], ['\x00\x0b\x00\x03\x01\x00', 'Unknown3', 0x100], ['\x00\x0c\x00\x03\x01\x00', 'Header1', 0x100], 42 | ['\x00\x0d\x00\x03\x01\x00', 'Header2', 0x100], ['\x00\x0e\x00\x03\x00\x40', 'Injection_Process', 0x40], ['\x00\x0f\x00\x03\x00\x80', 'PipeName', 0x80], 43 | ['\x00\x10\x00\x01\x00\x02', 'Year', 0x2], ['\x00\x11\x00\x01\x00\x02', 'Month', 0x2], ['\x00\x12\x00\x01\x00\x02', 'Day', 0x2], 44 | ['\x00\x13\x00\x02\x00\x04', 'DNS_idle', 0x4], ['\x00\x14\x00\x02\x00\x04', 'DNS_sleep(ms)', 0x2], ['\x00\x1a\x00\x03\x00\x10', 'Method1', 0x10], 45 | ['\x00\x1b\x00\x03\x00\x10', 'Method2', 0x10], ['\x00\x1c\x00\x02\x00\x04', 'Unknown4', 0x4], ['\x00\x1d\x00\x03\x00\x40', 'Spawnto_x86', 0x40], 46 | ['\x00\x1e\x00\x03\x00\x40', 'Spawnto_x64', 0x40], ['\x00\x1f\x00\x01\x00\x02', 'Unknown5', 0x2], ['\x00\x20\x00\x03\x00\x80', 'Proxy_HostName', 0x80], 47 | ['\x00\x21\x00\x03\x00\x40', 'Proxy_UserName', 0x40], ['\x00\x22\x00\x03\x00\x40', 'Proxy_Password', 0x40], ['\x00\x23\x00\x01\x00\x02', 'Proxy_AccessType', 0x2], 48 | ['\x00\x24\x00\x01\x00\x02', 'create_remote_thread', 0x2]] 49 | }] 50 | 51 | BEACONTYPE = {0x0: "0 (HTTP)", 0x1: "1 (Hybrid HTTP and DNS)", 0x8: "8 (HTTPS)"} 52 | ACCESSTYPE = {0x0: "0 (not use)", 0x1: "1 (use direct connection)", 0x2: "2 (use IE settings)", 0x4: "4 (use proxy server)"} 53 | 54 | 55 | class cobaltstrikeConfig(taskmods.DllList): 56 | 57 | """Detect processes infected with CobaltStrike malware""" 58 | 59 | @staticmethod 60 | def is_valid_profile(profile): 61 | return (profile.metadata.get('os', 'unknown') == 'windows'), profile.metadata.get('memory_model', '32bit') 62 | 63 | def get_vad_base(self, task, address): 64 | for vad in task.VadRoot.traverse(): 65 | if address >= vad.Start and address < vad.End: 66 | return vad.Start, vad.End 67 | 68 | return None 69 | 70 | def decode_config(self, cfg_blob): 71 | return "".join(chr(ord(cfg_offset) ^ 0x69) for cfg_offset in cfg_blob) 72 | 73 | def parse_config(self, cfg_blob, nw): 74 | 75 | p_data = OrderedDict() 76 | 77 | for pattern, name, size in nw['cfg_info']: 78 | if name.count('Port'): 79 | port = unpack_from('>H', cfg_blob, 0xE)[0] 80 | p_data[name] = port 81 | continue 82 | 83 | offset = cfg_blob.find(pattern) 84 | if offset == -1: 85 | p_data[name] = "" 86 | continue 87 | 88 | config_data = cfg_blob[offset + 6:offset + 6 + size] 89 | if name.count('Unknown'): 90 | p_data[name] = repr(config_data) 91 | continue 92 | 93 | if size == 2: 94 | if name.count('BeaconType'): 95 | p_data[name] = BEACONTYPE[unpack('>H', config_data)[0]] 96 | elif name.count('AccessType'): 97 | p_data[name] = ACCESSTYPE[unpack('>H', config_data)[0]] 98 | elif name.count('create_remote_thread'): 99 | if unpack('>H', config_data)[0] != 0: 100 | p_data[name] = "Enable" 101 | else: 102 | p_data[name] = "Disable" 103 | else: 104 | p_data[name] = unpack('>H', config_data)[0] 105 | elif size == 4: 106 | if name.count('DNS_idle'): 107 | p_data[name] = inet_ntoa(config_data) 108 | else: 109 | p_data[name] = unpack('>I', config_data)[0] 110 | else: 111 | if name.count('Header'): 112 | cfg_offset = 3 113 | flag = 0 114 | while 1: 115 | if cfg_offset > 255: 116 | break 117 | else: 118 | if config_data[cfg_offset] != '\x00': 119 | if config_data[cfg_offset + 1] != '\x00': 120 | if flag: 121 | name = name + "+" 122 | p_data[name] = config_data[(cfg_offset + 1):].split('\x00')[0] 123 | cfg_offset = config_data[cfg_offset:].find('\x00\x00\x00') + cfg_offset - 1 124 | flag += 1 125 | else: 126 | cfg_offset += 4 127 | continue 128 | else: 129 | cfg_offset += 4 130 | continue 131 | else: 132 | p_data[name] = config_data 133 | 134 | return p_data 135 | 136 | def calculate(self): 137 | 138 | if not has_yara: 139 | debug.error("Yara must be installed for this plugin") 140 | 141 | addr_space = utils.load_as(self._config) 142 | 143 | os, memory_model = self.is_valid_profile(addr_space.profile) 144 | if not os: 145 | debug.error("This command does not support the selected profile.") 146 | 147 | rules = yara.compile(sources=cobaltstrike_sig) 148 | 149 | for task in self.filter_tasks(tasks.pslist(addr_space)): 150 | scanner = malfind.VadYaraScanner(task=task, rules=rules) 151 | 152 | for hit, address in scanner.scan(): 153 | 154 | vad_base_addr, end = self.get_vad_base(task, address) 155 | proc_addr_space = task.get_process_address_space() 156 | data = proc_addr_space.zread(vad_base_addr, end - vad_base_addr) 157 | 158 | config_data = [] 159 | 160 | for nw in CONF_PATTERNS: 161 | cfg_addr = data.find(nw['pattern']) 162 | if cfg_addr != -1: 163 | break 164 | else: 165 | continue 166 | 167 | cfg_blob = data[cfg_addr:cfg_addr + nw['cfg_size']] 168 | config_data.append(self.parse_config(self.decode_config(cfg_blob), nw)) 169 | 170 | yield task, vad_base_addr, end, hit, memory_model, config_data 171 | break 172 | 173 | def render_text(self, outfd, data): 174 | 175 | delim = '-' * 70 176 | 177 | for task, start, end, malname, memory_model, config_data in data: 178 | outfd.write("{0}\n".format(delim)) 179 | outfd.write("Process: {0} ({1})\n\n".format(task.ImageFileName, task.UniqueProcessId)) 180 | 181 | outfd.write("[Config Info]\n") 182 | for p_data in config_data: 183 | for id, param in p_data.items(): 184 | outfd.write("{0:<22}: {1}\n".format(id, param)) 185 | -------------------------------------------------------------------------------- /utils/formbookscan.py: -------------------------------------------------------------------------------- 1 | # Detecting Formbook for Volatility 2 | # 3 | # LICENSE 4 | # Please refer to the LICENSE.txt in the https://github.com/JPCERTCC/MalConfScan/ 5 | # 6 | # How to use: 7 | # 1. cd "Volatility Folder" 8 | # 2. mv formbookscan.py volatility/plugins/malware 9 | # 3. python vol.py formbookconfig -f images.mem --profile=Win7SP1x64 10 | 11 | import volatility.plugins.taskmods as taskmods 12 | import volatility.win32.tasks as tasks 13 | import volatility.utils as utils 14 | import volatility.debug as debug 15 | import volatility.plugins.malware.malfind as malfind 16 | import re 17 | import pefile 18 | from Crypto.Hash import SHA 19 | from struct import unpack, unpack_from, pack 20 | from collections import OrderedDict 21 | from formbook_decryption import FormBookDecryption 22 | 23 | try: 24 | import yara 25 | has_yara = True 26 | except ImportError: 27 | has_yara = False 28 | 29 | formbook_sig = { 30 | 'namespace1' : 'rule Formbook { \ 31 | strings: \ 32 | $sqlite3step = { 68 34 1c 7b e1 } \ 33 | $sqlite3text = { 68 38 2a 90 c5 } \ 34 | $sqlite3blob = { 68 53 d8 7f 8c } \ 35 | condition: all of them}' 36 | } 37 | 38 | # Config pattern 39 | CONFIG_PATTERNS = [re.compile("\x83\xc4\x0c\x6a\x14\xe8(....)\x83\xc0\x02\x50\x8d(..)\x51\xe8(....)\x83\xc4\x0c\x6a\x14\xe8(....)\x83\xc0\x02\x50\x8d(..)\x52", re.DOTALL)] 40 | 41 | # Hashs pattern 42 | HASHS_PATTERNS = [re.compile("\x68(.)(\x02|\x03)\x00\x00\x8d(...)\x00\x00\xe8", re.DOTALL)] 43 | 44 | # Strings pattern 45 | STRINGS_PATTERNS = [re.compile("\x6a\x00\x50\xc6\x85(....)\x00\xe8(....)\x83\xc4\x0c\x68(..)\x00\x00\xe8", re.DOTALL)] 46 | 47 | 48 | class formbookConfig(taskmods.DllList): 49 | """Parse the Formbook configuration""" 50 | 51 | @staticmethod 52 | def is_valid_profile(profile): 53 | return (profile.metadata.get('os', 'unknown') == 'windows'), profile.metadata.get('memory_model', '32bit') 54 | 55 | def get_vad_base(self, task, address): 56 | for vad in task.VadRoot.traverse(): 57 | if address >= vad.Start and address < vad.End: 58 | return vad.Start, vad.End 59 | 60 | return None 61 | 62 | def sha1_revert(self, digest): 63 | tuples = unpack("I", item) 67 | return output_hash 68 | 69 | def formbook_compute_sha1(self, input_buffer): 70 | sha1 = SHA.new() 71 | sha1.update(input_buffer) 72 | return self.sha1_revert(sha1.digest()) 73 | 74 | def formbook_decrypt_strings(self, fb_decrypt, p_data, key, encrypted_strings): 75 | offset = 0 76 | i = 0 77 | while offset < len(encrypted_strings): 78 | str_len = ord(encrypted_strings[offset]) 79 | offset += 1 80 | dec_str = fb_decrypt.decrypt_func2(encrypted_strings[offset:offset + str_len], key) 81 | dec_str = dec_str[:-1] # remove '\0' character 82 | p_data["Encoded string " + str(i)] = dec_str 83 | offset += str_len 84 | i += 1 85 | 86 | return p_data 87 | 88 | def formbook_decrypt(self, key1, key2, config, config_size, strings_data, strings_size, url_size, hashs_data, hashs_size): 89 | fb_decrypt = FormBookDecryption() 90 | p_data = OrderedDict() 91 | 92 | rc4_key_one = fb_decrypt.decrypt_func1(key1, 0x14) 93 | rc4_key_two = fb_decrypt.decrypt_func1(key2, 0x14) 94 | encbuf2_s1 = fb_decrypt.decrypt_func1(hashs_data, hashs_size) 95 | encbuf8_s1 = fb_decrypt.decrypt_func1(config, config_size) 96 | encbuf9_s1 = fb_decrypt.decrypt_func1(strings_data, strings_size) 97 | 98 | rc4_key_1 = self.formbook_compute_sha1(encbuf8_s1) 99 | rc4_key_2 = self.formbook_compute_sha1(encbuf9_s1) 100 | rc4_key_3 = self.formbook_compute_sha1(rc4_key_two) 101 | encbuf2_s2 = fb_decrypt.decrypt_func2(encbuf2_s1, rc4_key_1) 102 | encbuf8_s2 = fb_decrypt.decrypt_func2(encbuf8_s1, rc4_key_2) 103 | 104 | n = 1 105 | for i in xrange(config_size): 106 | encrypted_c2c_uri = encbuf8_s2[i:i + url_size] 107 | encrypted_c2c_uri = fb_decrypt.decrypt_func2(encrypted_c2c_uri, rc4_key_two) 108 | c2c_uri = fb_decrypt.decrypt_func2(encrypted_c2c_uri, rc4_key_one) 109 | if "www." in c2c_uri: 110 | p_data["C&C URI " + str(n)] = c2c_uri 111 | n += 1 112 | 113 | encrypted_hashes_array = fb_decrypt.decrypt_func2(encbuf2_s2, rc4_key_3) 114 | rc4_key_pre_final = self.formbook_compute_sha1(encrypted_hashes_array) 115 | rc4_key_final = fb_decrypt.decrypt_func2(rc4_key_two, rc4_key_pre_final) 116 | 117 | p_data = self.formbook_decrypt_strings(fb_decrypt, p_data, rc4_key_final, encbuf9_s1) 118 | 119 | return p_data 120 | 121 | def calculate(self): 122 | 123 | if not has_yara: 124 | debug.error("Yara must be installed for this plugin") 125 | 126 | addr_space = utils.load_as(self._config) 127 | 128 | os, memory_model = self.is_valid_profile(addr_space.profile) 129 | if not os: 130 | debug.error("This command does not support the selected profile.") 131 | 132 | rules = yara.compile(sources=formbook_sig) 133 | 134 | for task in self.filter_tasks(tasks.pslist(addr_space)): 135 | scanner = malfind.VadYaraScanner(task=task, rules=rules) 136 | 137 | for hit, address in scanner.scan(): 138 | 139 | vad_base_addr, end = self.get_vad_base(task, address) 140 | proc_addr_space = task.get_process_address_space() 141 | data = proc_addr_space.zread(vad_base_addr, end - vad_base_addr) 142 | 143 | config_data = [] 144 | try: 145 | pe = pefile.PE(data=data) 146 | except: 147 | continue 148 | 149 | for pattern in CONFIG_PATTERNS: 150 | offset = re.search(pattern, data).start() 151 | 152 | offset += 6 153 | key1_offset = unpack("=I", data[offset:offset + 4])[0] + offset + 11 154 | key1 = data[key1_offset:key1_offset + (0x14 * 2)] 155 | offset += 23 156 | key2_offset = unpack("=I", data[offset:offset + 4])[0] + offset + 11 157 | key2 = data[key2_offset:key2_offset + (0x14 * 2)] 158 | offset += 21 159 | config_size = unpack("=I", data[offset:offset + 4])[0] 160 | offset += 5 161 | config_offset = unpack("=I", data[offset:offset + 4])[0] + offset + 11 162 | config = data[config_offset:config_offset + (config_size * 2)] 163 | offset += 33 164 | url_size = unpack("b", data[offset])[0] 165 | 166 | for pattern in STRINGS_PATTERNS: 167 | offset = re.search(pattern, data).start() 168 | 169 | offset += 19 170 | strings_size = unpack("=I", data[offset:offset + 4])[0] 171 | offset += 5 172 | strings_offset = unpack("=I", data[offset:offset + 4])[0] + offset + 11 173 | strings_data = data[strings_offset:strings_offset + (strings_size * 2)] 174 | 175 | for pattern in HASHS_PATTERNS: 176 | offset = re.search(pattern, data).start() 177 | 178 | offset += 1 179 | hashs_size = unpack("=I", data[offset:offset + 4])[0] 180 | offset += 11 181 | hashs_offset = unpack("=I", data[offset:offset + 4])[0] + offset + 11 182 | hashs_data = data[hashs_offset:hashs_offset + (hashs_size * 2)] 183 | 184 | config_data.append(self.formbook_decrypt(key1, key2, config, config_size, strings_data, 185 | strings_size, url_size, hashs_data, hashs_size)) 186 | 187 | yield task, vad_base_addr, end, hit, memory_model, config_data 188 | break 189 | 190 | def render_text(self, outfd, data): 191 | 192 | delim = '-' * 70 193 | 194 | for task, start, end, malname, memory_model, config_data in data: 195 | outfd.write("{0}\n".format(delim)) 196 | outfd.write("Process: {0} ({1})\n\n".format(task.ImageFileName, task.UniqueProcessId)) 197 | 198 | outfd.write("[Config Info]\n") 199 | for p_data in config_data: 200 | for id, param in p_data.items(): 201 | outfd.write("{0:<16}: {1}\n".format(id, param)) 202 | -------------------------------------------------------------------------------- /utils/redleavesscan.py: -------------------------------------------------------------------------------- 1 | # Detecting RedLeaves for Volatility 2 | # 3 | # LICENSE 4 | # Please refer to the LICENSE.txt in the https://github.com/JPCERTCC/MalConfScan/ 5 | # 6 | # How to use: 7 | # 1. cd "Volatility Folder" 8 | # 2. mv redleavesscan.py volatility/plugins/malware 9 | # 3. python vol.py redleavesconfig -f images.mem --profile=Win7SP1x64 10 | 11 | import volatility.plugins.taskmods as taskmods 12 | import volatility.win32.tasks as tasks 13 | import volatility.utils as utils 14 | import volatility.debug as debug 15 | import volatility.plugins.malware.malfind as malfind 16 | import re 17 | from struct import unpack, unpack_from 18 | from collections import OrderedDict 19 | 20 | try: 21 | import yara 22 | has_yara = True 23 | except ImportError: 24 | has_yara = False 25 | 26 | redleaves_sig = { 27 | 'namespace1' : 'rule RedLeaves { \ 28 | strings: \ 29 | $v1 = "red_autumnal_leaves_dllmain.dll" \ 30 | $b1 = { FF FF 90 00 } \ 31 | condition: $v1 and $b1 at 0}', 32 | 'namespace2' : 'rule Himawari { \ 33 | strings: \ 34 | $h1 = "himawariA" \ 35 | $h2 = "himawariB" \ 36 | $h3 = "HimawariDemo" \ 37 | condition: $h1 and $h2 and $h3}', 38 | 'namespace3' : 'rule Lavender { \ 39 | strings: \ 40 | $l1 = {C7 ?? ?? 4C 41 56 45} \ 41 | $l2 = {C7 ?? ?? 4E 44 45 52} \ 42 | condition: $l1 and $l2}', 43 | 'namespace4' : 'rule Armadill { \ 44 | strings: \ 45 | $a1 = {C7 ?? ?? 41 72 6D 61 } \ 46 | $a2 = {C7 ?? ?? 64 69 6C 6C } \ 47 | condition: $a1 and $a2}', 48 | 'namespace5' : 'rule zark20rk { \ 49 | strings: \ 50 | $a1 = {C7 ?? ?? 7A 61 72 6B } \ 51 | $a2 = {C7 ?? ?? 32 30 72 6B } \ 52 | condition: $a1 and $a2}' 53 | } 54 | 55 | CONF_PATTERNS = {"RedLeaves": re.compile("\x68\x88\x13\x00\x00\xFF", re.DOTALL), 56 | "Himawari": re.compile("\x68\x70\x03\x00\x00\xBF", re.DOTALL), 57 | "Lavender": re.compile("\x68\x70\x03\x00\x00\xBF", re.DOTALL), 58 | "Armadill": re.compile("\x68\x70\x03\x00\x00\xBF", re.DOTALL), 59 | "zark20rk": re.compile("\x68\x70\x03\x00\x00\x8D", re.DOTALL), 60 | } 61 | 62 | CONNECT_MODE = {1: 'TCP', 2: 'HTTP', 3: 'HTTPS', 4: 'TCP and HTTP'} 63 | 64 | 65 | class redleavesConfig(taskmods.DllList): 66 | """Detect processes infected with redleaves malware""" 67 | 68 | @staticmethod 69 | def is_valid_profile(profile): 70 | return (profile.metadata.get('os', 'unknown') == 'windows'), profile.metadata.get('memory_model', '32bit') 71 | 72 | def get_vad_base(self, task, address): 73 | for vad in task.VadRoot.traverse(): 74 | if address >= vad.Start and address < vad.End: 75 | return vad.Start, vad.End 76 | 77 | return None 78 | 79 | def parse_config(self, cfg_blob, cfg_sz, cfg_addr): 80 | 81 | p_data = OrderedDict() 82 | 83 | p_data["Server1"] = unpack_from('<64s', cfg_blob, 0x0)[0] 84 | p_data["Server2"] = unpack_from('<64s', cfg_blob, 0x40)[0] 85 | p_data["Server3"] = unpack_from('<64s', cfg_blob, 0x80)[0] 86 | p_data["Port"] = unpack_from(' 0: 174 | config_data.append(self.parse_config(config, config_size, config_addr)) 175 | 176 | if str(hit) in ["Himawari", "Lavender", "Armadill", "zark20rk"]: 177 | offset_conf += 6 178 | if str(hit) in ["zark20rk"]: 179 | offset_conf += 6 180 | config_size = 880 181 | 182 | # get address 183 | (config_addr, ) = unpack("=I", data[offset_conf:offset_conf + 4]) 184 | 185 | if config_addr < vad_base_addr: 186 | continue 187 | 188 | config_addr -= vad_base_addr 189 | config = data[config_addr:config_addr + config_size] 190 | if len(config) > 0: 191 | config_data.append(self.parse_config_himawari(config, config_size, config_addr)) 192 | 193 | yield task, vad_base_addr, end, hit, memory_model, config_data 194 | break 195 | 196 | def render_text(self, outfd, data): 197 | 198 | delim = '-' * 70 199 | 200 | for task, start, end, malname, memory_model, config_data in data: 201 | outfd.write("{0}\n".format(delim)) 202 | outfd.write("Process: {0} ({1})\n\n".format(task.ImageFileName, task.UniqueProcessId)) 203 | 204 | outfd.write("[Config Info]\n") 205 | for p_data in config_data: 206 | for id, param in p_data.items(): 207 | outfd.write("{0:<16}: {1}\n".format(id, param)) 208 | -------------------------------------------------------------------------------- /utils/tscookiescan.py: -------------------------------------------------------------------------------- 1 | # Detecting TSCookie for Volatility 2 | # 3 | # LICENSE 4 | # Please refer to the LICENSE.txt in the https://github.com/JPCERTCC/MalConfScan/ 5 | # 6 | # How to use: 7 | # 1. cd "Volatility Folder" 8 | # 2. mv tscookiescan.py volatility/plugins/malware 9 | # 3. python vol.py tscookieconfig -f images.mem --profile=Win7SP1x64 10 | 11 | import volatility.plugins.taskmods as taskmods 12 | import volatility.win32.tasks as tasks 13 | import volatility.utils as utils 14 | import volatility.debug as debug 15 | import volatility.plugins.malware.malfind as malfind 16 | import re 17 | import pefile 18 | from struct import unpack, unpack_from 19 | from collections import OrderedDict 20 | 21 | try: 22 | import yara 23 | has_yara = True 24 | except ImportError: 25 | has_yara = False 26 | 27 | tscookie_sig = { 28 | 'namespace1' : 'rule TSCookie { \ 29 | strings: \ 30 | $v1 = "Mozilla/4.0 (compatible; MSIE 8.0; Win32)" wide\ 31 | $mz = { 4D 5A 90 00 } \ 32 | $b1 = { 68 D4 08 00 00 } \ 33 | condition: all of them}', 34 | 'namespace2' : 'rule TSC_Loader { \ 35 | strings: \ 36 | $v1 = "Mozilla/4.0 (compatible; MSIE 8.0; Win32)" wide\ 37 | $mz = { 4D 5A 90 00 } \ 38 | $b1 = { 68 78 0B 00 00 } \ 39 | condition: all of them}' 40 | } 41 | 42 | # MZ Header 43 | MZ_HEADER = b"\x4D\x5A\x90\x00" 44 | 45 | # Config pattern 46 | CONFIG_PATTERNS = [re.compile("\xC3\x90\x68\x00(...)\xE8(....)\x59\x6A\x01\x58\xC3", re.DOTALL), 47 | re.compile("\x6A\x04\x68(....)\x8D(.....)\x56\x50\xE8", re.DOTALL), 48 | re.compile("\x00\x00\x68(....)\xE8(....)\x59\x59\x6A\x01", re.DOTALL), 49 | re.compile("\x68(....)\xE8(....)\x59\x6A\x01\x58\xC3", re.DOTALL), 50 | re.compile("\x68(....)\xE8(....)\x59", re.DOTALL)] 51 | 52 | CONNECT_MODE = {0: 'TCP', 1: 'HTTP with Credentials', 2: 'HTTP with Credentials', 3: 'HTTP with Credentials', 53 | 5: 'HTTP', 6: 'HTTPS', 7: 'HTTPS', 8: 'HTTPS'} 54 | PROXY_MODE = {0: 'Detect proxy settings', 1: 'Use config'} 55 | INJECTION_MODE = {0 : 'Create process' , 1 : 'Injection running process'} 56 | PROCESS_NAME = {0 : 'svchost.exe', 1 : 'iexplorer.exe', 2 : 'explorer.exe', 3 : 'Default browser' , 4: 'Setting process'} 57 | 58 | class tscookieConfig(taskmods.DllList): 59 | """Parse the TSCookie configuration""" 60 | 61 | @staticmethod 62 | def is_valid_profile(profile): 63 | return (profile.metadata.get('os', 'unknown') == 'windows'), profile.metadata.get('memory_model', '32bit') 64 | 65 | def get_vad_base(self, task, address): 66 | for vad in task.VadRoot.traverse(): 67 | if address >= vad.Start and address < vad.End: 68 | return vad.Start, vad.End 69 | 70 | return None 71 | 72 | def rc4(self, data, key): 73 | x = 0 74 | box = range(256) 75 | for i in range(256): 76 | x = (x + box[i] + ord(key[i % len(key)])) % 256 77 | box[i], box[x] = box[x], box[i] 78 | x = 0 79 | y = 0 80 | out = [] 81 | for char in data: 82 | x = (x + 1) % 256 83 | y = (y + box[x]) % 256 84 | box[x], box[y] = box[y], box[x] 85 | out.append(chr(ord(char) ^ box[(box[x] + box[y]) % 256])) 86 | 87 | return ''.join(out) 88 | 89 | def parse_config(self, config): 90 | p_data = OrderedDict() 91 | for i in xrange(4): 92 | if config[0x10 + 0x100 * i] != "\x00": 93 | p_data["Server " + str(i)] = unpack_from("<240s", config, 0x10 + 0x100 * i)[0].replace("\0", "") 94 | p_data["Server " + str(i) + " (port 1)"] = unpack_from("I", config, 0x604)[0]) 101 | if len(config) > 0x89C: 102 | p_data["Sleep time"] = unpack_from("I", config, 0x400)[0]) 110 | p_data["Sleep count"] = unpack_from(" 0: 199 | if "TSCookie" in str(hit): 200 | config_data.append(self.parse_config(config)) 201 | else: 202 | config_data.append(self.parse_loader_config(config)) 203 | except: 204 | print("[!] Not found config data.\n") 205 | 206 | yield task, vad_base_addr, end, hit, memory_model, config_data 207 | break 208 | 209 | def render_text(self, outfd, data): 210 | 211 | delim = '-' * 70 212 | 213 | for task, start, end, malname, memory_model, config_data in data: 214 | outfd.write("{0}\n".format(delim)) 215 | outfd.write("Process: {0} ({1})\n\n".format(task.ImageFileName, task.UniqueProcessId)) 216 | 217 | outfd.write("[Config Info]\n") 218 | for p_data in config_data: 219 | for id, param in p_data.items(): 220 | outfd.write("{0:<25}: {1}\n".format(id, param)) 221 | -------------------------------------------------------------------------------- /utils/formbook_decryption.py: -------------------------------------------------------------------------------- 1 | # https://github.com/tildedennis/malware/blob/master/formbook/formbook_decryption.py 2 | 3 | from Crypto.Cipher import ARC4 4 | 5 | 6 | class FormBookDecryption: 7 | 8 | def decrypt_func1(self, encbuf, plainbuf_len): 9 | plainbuf = [] 10 | 11 | ebl = [ord(b) for b in encbuf] 12 | 13 | if ebl[0] != 0x55 or ebl[1] != 0x8b: 14 | print "doesn't start with a function prologue" 15 | return 16 | 17 | ebl = ebl[3:] 18 | ei = 0 19 | 20 | while len(plainbuf) < plainbuf_len: 21 | if ((ebl[ei] - 64) & 0xff) > 31: 22 | if ((ebl[ei] - 112) & 0xff) > 15: 23 | plainbuf, ei = self.decrypt_func1_transform(plainbuf, ebl, ei) 24 | else: 25 | ei += 2 26 | else: 27 | plainbuf, ei = self.offset0_byte_1byte(plainbuf, ebl, ei) 28 | 29 | 30 | return "".join([chr(b & 0xff) for b in plainbuf]) 31 | 32 | 33 | def decrypt_func1_transform(self, plainbuf, ebl, ei): 34 | if ebl[ei] <= 3: 35 | return self.offset2_dword_6bytes(plainbuf, ebl, ei) 36 | 37 | if ebl[ei] == 4: 38 | return self.offset1_byte_2bytes(plainbuf, ebl, ei) 39 | 40 | if ebl[ei] == 5: 41 | return self.offset1_dword_5bytes(plainbuf, ebl, ei) 42 | 43 | if ((ebl[ei] - 8) & 0xff) <= 3: 44 | return self.offset2_dword_6bytes(plainbuf, ebl, ei) 45 | 46 | if ebl[ei] == 12: 47 | return self.offset1_byte_2bytes(plainbuf, ebl, ei) 48 | 49 | if ebl[ei] == 13: 50 | return self.offset1_dword_5bytes(plainbuf, ebl, ei) 51 | 52 | if ebl[ei] == 15: 53 | ei += 6 54 | return plainbuf, ei 55 | 56 | if ((ebl[ei] - 16) & 0xff) <= 3: 57 | return self.offset2_dword_6bytes(plainbuf, ebl, ei) 58 | 59 | if ebl[ei] == 20: 60 | return self.offset1_byte_2bytes(plainbuf, ebl, ei) 61 | 62 | if ebl[ei] == 21: 63 | return self.offset1_dword_5bytes(plainbuf, ebl, ei) 64 | 65 | if ((ebl[ei] - 24) & 0xff) <= 3: 66 | return self.offset2_dword_6bytes(plainbuf, ebl, ei) 67 | 68 | if ebl[ei] == 28: 69 | return self.offset1_byte_2bytes(plainbuf, ebl, ei) 70 | 71 | if ebl[ei] == 29: 72 | return self.offset1_dword_5bytes(plainbuf, ebl, ei) 73 | 74 | if ((ebl[ei] - 32) & 0xff) <= 3: 75 | return self.offset2_dword_6bytes(plainbuf, ebl, ei) 76 | 77 | if ebl[ei] == 36: 78 | return self.offset1_byte_2bytes(plainbuf, ebl, ei) 79 | 80 | if ebl[ei] == 37: 81 | return self.offset1_dword_5bytes(plainbuf, ebl, ei) 82 | 83 | if ((ebl[ei] - 40) & 0xff) <= 3: 84 | return self.offset2_dword_6bytes(plainbuf, ebl, ei) 85 | 86 | if ebl[ei] == 44: 87 | return self.offset1_byte_2bytes(plainbuf, ebl, ei) 88 | 89 | if ebl[ei] == 45: 90 | return self.offset1_dword_5bytes(plainbuf, ebl, ei) 91 | 92 | if ((ebl[ei] - 48) & 0xff) <= 3: 93 | return self.offset2_dword_6bytes(plainbuf, ebl, ei) 94 | 95 | if ebl[ei] == 52: 96 | return self.offset1_byte_2bytes(plainbuf, ebl, ei) 97 | 98 | if ebl[ei] == 53: 99 | return self.offset1_dword_5bytes(plainbuf, ebl, ei) 100 | 101 | if ((ebl[ei] - 56) & 0xff) <= 3: 102 | return self.offset2_dword_6bytes(plainbuf, ebl, ei) 103 | 104 | if ebl[ei] == 60: 105 | return self.offset1_byte_2bytes(plainbuf, ebl, ei) 106 | 107 | if ebl[ei] == 61: 108 | return self.offset1_dword_5bytes(plainbuf, ebl, ei) 109 | 110 | if ebl[ei] == 102: 111 | if ebl[ei+1] == 106: 112 | plainbuf += ebl[ei+1:ei+1+2] 113 | ei += 3 114 | 115 | if ebl[ei+1] == 104 or ebl[ei+1] == 184: 116 | plainbuf, ei = self.offset2_short_4bytes(plainbuf, ebl, ei) 117 | else: 118 | ei += 1 119 | 120 | return plainbuf, ei 121 | 122 | if ebl[ei] == 104: 123 | return self.offset1_dword_5bytes(plainbuf, ebl, ei) 124 | 125 | if ebl[ei] == 105: 126 | plainbuf += ebl[ei+2:ei+2+4] 127 | plainbuf += ebl[ei+6:ei+6+2] 128 | ei += 10 129 | return plainbuf, ei 130 | 131 | if ebl[ei] == 106: 132 | offset = ebl[ei+1] 133 | if (offset & 0x80) != 0: 134 | offset |= 0xffffff00 135 | plainbuf += ebl[offset:offset+4] 136 | ei += 2 137 | return plainbuf, ei 138 | 139 | if ebl[ei] == 107: 140 | plainbuf += ebl[ei+2:ei+2+4] 141 | plainbuf += ebl[ei+6:ei+6+2] 142 | ei += 7 143 | return plainbuf, ei 144 | 145 | if ebl[ei] == 128: 146 | if ebl[ei+1] == 5: 147 | plainbuf += ebl[ei+2:ei+2+4] 148 | ei += 7 149 | else: 150 | plainbuf += ebl[ei+2:ei+2+1] 151 | ei += 3 152 | return plainbuf, ei 153 | 154 | if ebl[ei] == 129: 155 | return self.offset2_dword_6bytes(plainbuf, ebl, ei) 156 | 157 | if ebl[ei] == 131: 158 | offset = ebl[ei+2] 159 | if (offset & 0x80) != 0: 160 | offset |= 0xffffff00 161 | plainbuf += ebl[offset:offset+4] 162 | ei += 3 163 | 164 | if ((ebl[ei] + 124) & 0xff) <= 7: 165 | return self.offset2_dword_6bytes(plainbuf, ebl, ei) 166 | 167 | if ebl[ei] == 141: 168 | return self.offset2_dword_6bytes(plainbuf, ebl, ei) 169 | 170 | if ebl[ei] == 143: 171 | return self.offset2_dword_6bytes(plainbuf, ebl, ei) 172 | 173 | if ebl[ei] == 144: 174 | return self.offset0_byte_1byte(plainbuf, ebl, ei) 175 | 176 | if ((ebl[ei] + 96) & 0xff) <= 3: 177 | return self.offset1_dword_5bytes(plainbuf, ebl, ei) 178 | 179 | if ((ebl[ei] + 92) & 0xff) <= 3: 180 | return self.offset0_byte_1byte(plainbuf, ebl, ei) 181 | 182 | if ebl[ei] == 168: 183 | return self.offset1_byte_2bytes(plainbuf, ebl, ei) 184 | 185 | if ebl[ei] == 169: 186 | return self.offset1_dword_5bytes(plainbuf, ebl, ei) 187 | 188 | if ((ebl[ei] + 86) & 0xff) <= 5: 189 | return self.offset0_byte_1byte(plainbuf, ebl, ei) 190 | 191 | if ((ebl[ei] + 80) & 0xff) <= 7: 192 | return self.offset1_byte_2bytes(plainbuf, ebl, ei) 193 | 194 | if ((ebl[ei] + 72) & 0xff) <= 7: 195 | return self.offset1_dword_5bytes(plainbuf, ebl, ei) 196 | 197 | if ebl[ei] == 192: 198 | return self.offset2_dword_7bytes(plainbuf, ebl, ei) 199 | 200 | if ebl[ei] == 193: 201 | return self.offset2_dword_7bytes(plainbuf, ebl, ei) 202 | 203 | if ebl[ei] == 194: 204 | return self.offset1_short_3bytes(plainbuf, ebl, ei) 205 | 206 | if ebl[ei] == 195: 207 | return self.offset0_byte_1byte(plainbuf, ebl, ei) 208 | 209 | if ebl[ei] == 208: 210 | return self.offset2_dword_6bytes(plainbuf, ebl, ei) 211 | 212 | if ebl[ei] == 209: 213 | return self.offset2_dword_6bytes(plainbuf, ebl, ei) 214 | 215 | if ebl[ei] == 232 or ebl[ei] == 233: 216 | ei += 5 217 | return plainbuf, ei 218 | 219 | if ebl[ei] == 235: 220 | ei += 2 221 | return plainbuf, ei 222 | 223 | if ebl[ei] == 242: 224 | return self.offset0_byte_1byte(plainbuf, ebl, ei) 225 | 226 | if ebl[ei] == 246: 227 | return self.offset2_byte_3bytes(plainbuf, ebl, ei) 228 | 229 | if ebl[ei] == 247: 230 | return self.offset2_dword_6bytes(plainbuf, ebl, ei) 231 | 232 | if ebl[ei] == 255: 233 | if ebl[ei + 1] == 53: 234 | return self.offset2_dword_6bytes(plainbuf, ebl, ei) 235 | 236 | return plainbuf, ei 237 | 238 | 239 | def offset0_byte_1byte(self, plainbuf, ebl, ei): 240 | plainbuf += [ebl[ei]] 241 | ei += 1 242 | return plainbuf, ei 243 | 244 | 245 | def offset1_byte_2bytes(self, plainbuf, ebl, ei): 246 | plainbuf += ebl[ei+1:ei+1+1] 247 | ei += 2 248 | return plainbuf, ei 249 | 250 | 251 | def offset1_short_3bytes(self, plainbuf, ebl, ei): 252 | plainbuf += ebl[ei+1:ei+1+2] 253 | ei += 3 254 | return plainbuf, ei 255 | 256 | 257 | def offset2_byte_3bytes(self, plainbuf, ebl, ei): 258 | plainbuf += ebl[ei+2:ei+2+1] 259 | ei += 3 260 | return plainbuf, ei 261 | 262 | 263 | def offset2_short_4bytes(self, plainbuf, ebl, ei): 264 | plainbuf += ebl[ei+2:ei+2+2] 265 | ei += 4 266 | return plainbuf, ei 267 | 268 | 269 | def offset1_dword_5bytes(self, plainbuf, ebl, ei): 270 | plainbuf += ebl[ei+1:ei+1+4] 271 | ei += 5 272 | return plainbuf, ei 273 | 274 | 275 | def offset2_dword_6bytes(self, plainbuf, ebl, ei): 276 | plainbuf += ebl[ei+2:ei+2+4] 277 | ei += 6 278 | return plainbuf, ei 279 | 280 | 281 | def offset2_dword_7bytes(self, plainbuf, ebl, ei): 282 | plainbuf += ebl[ei+2:ei+2+4] 283 | ei += 7 284 | return plainbuf, ei 285 | 286 | 287 | def decrypt_func2(self, encbuf, key): 288 | ebl = [ord(b) for b in encbuf] 289 | 290 | # transform 1 291 | for i in range(len(encbuf) - 1, 0, -1): 292 | ebl[i-1] -= ebl[i] 293 | 294 | # transform 2 295 | for i in range(0, len(encbuf) -1): 296 | ebl[i] -= ebl[i+1] 297 | 298 | # rc4 299 | round2 = "".join([chr(b & 0xff) for b in ebl]) 300 | arc4 = ARC4.new(key) 301 | round3 = arc4.decrypt(round2) 302 | 303 | round3l = [ord(b) for b in round3] 304 | 305 | # transform 3 306 | for i in range(len(encbuf) - 1, 0, -1): 307 | round3l[i-1] -= round3l[i] 308 | 309 | # transform 4 310 | for i in range(0, len(encbuf) -1): 311 | round3l[i] -= round3l[i+1] 312 | 313 | plainbuf = "".join([chr(b & 0xff) for b in round3l]) 314 | 315 | return plainbuf 316 | -------------------------------------------------------------------------------- /utils/datperscan.py: -------------------------------------------------------------------------------- 1 | # Detecting Datper for Volatility 2 | # 3 | # LICENSE 4 | # Please refer to the LICENSE.txt in the https://github.com/JPCERTCC/MalConfScan/ 5 | # 6 | # How to use: 7 | # 1. cd "Volatility Folder" 8 | # 2. mv datperscan.py volatility/plugins/malware 9 | # 3. python vol.py datperconfig -f images.mem --profile=Win7SP1x64 10 | 11 | import volatility.plugins.taskmods as taskmods 12 | import volatility.win32.tasks as tasks 13 | import volatility.utils as utils 14 | import volatility.debug as debug 15 | import volatility.plugins.malware.malfind as malfind 16 | import re 17 | import pefile 18 | from struct import unpack, unpack_from 19 | from collections import OrderedDict 20 | 21 | try: 22 | import yara 23 | has_yara = True 24 | except ImportError: 25 | has_yara = False 26 | 27 | datper_sig = { 28 | 'namespace1' : 'rule Datper { \ 29 | strings: \ 30 | $a1 = { E8 03 00 00 } \ 31 | $b1 = "|||" \ 32 | $c1 = "Content-Type: application/x-www-form-urlencoded" \ 33 | $delphi = "Borland\\Delphi" ascii wide \ 34 | $push7530h64 = { C7 C1 30 75 00 00 } \ 35 | $push7530h = { 68 30 75 00 00 } \ 36 | condition: $a1 and $b1 and $c1 and $delphi and ($push7530h64 or $push7530h)}' 37 | } 38 | 39 | CONFIG_PATTERNS = [ 40 | re.compile("\xB8(....)(\xBA\xE8\x03\x00\x00)", re.DOTALL), # mov eax, qword ptr config_offset;mov edx 0x3e8 41 | re.compile("\xB8(....)(\x75\x00\xBA\xE8\x03\x00\x00)", re.DOTALL), # mov eax, qword ptr config_offset;jnz short $+2;mov edx 0x3e8 42 | re.compile("\x48\x8D\x0D(....)(\xC7\xC2\xE8\x03\x00\x00)", re.DOTALL) # lea rax, qword ptr config_offset;mov edx 0x3e8 43 | ] 44 | 45 | RC4KEY = ["d4n[6h}8o<09,d(21i`t4n$]hx%.h,hd", 46 | "B3uT16@qs\l,!GdSevH=Y(;7Ady$jl\e", 47 | "V7oT1@@qr\\t,!GOSKvb=p(;3Akb$rl\\a" 48 | ] 49 | 50 | idx_list = { 51 | 0: "ID", 52 | 1: "URL", 53 | 2: "Sleep time(s)", 54 | 3: "Mutex", 55 | 4: "Proxy server", 56 | 5: "Proxy port", 57 | 6: "Unknown", 58 | 7: "Unknown", 59 | 8: "Startup time(h)", 60 | 9: "End time(h)", 61 | 10: "Unknown", 62 | 11: "User-Agent", 63 | 12: "RSA key(e + modules)" 64 | } 65 | 66 | CONFSIZE = 0x3F8 67 | config_delimiter = ["|||", "[|-]"] 68 | 69 | 70 | class datperConfig(taskmods.DllList): 71 | """Parse the Datper configuration""" 72 | 73 | @staticmethod 74 | def is_valid_profile(profile): 75 | return (profile.metadata.get('os', 'unknown') == 'windows'), profile.metadata.get('memory_model', '32bit') 76 | 77 | def get_vad_base(self, task, address): 78 | for vad in task.VadRoot.traverse(): 79 | if address >= vad.Start and address < vad.End: 80 | return vad.Start, vad.End 81 | 82 | return None 83 | 84 | # Custom RC4 use sbox seed 85 | def custom_rc4(self, data, key, box_seed): 86 | x = 0 87 | box = range(256) 88 | if box_seed != 0: 89 | for i in range(256): 90 | box[i] = (i + box_seed) & 0xFF 91 | 92 | for i in range(256): 93 | x = (x + box[i] + ord(key[i % len(key)])) % 256 94 | box[i], box[x] = box[x], box[i] 95 | x = 0 96 | y = 0 97 | out = [] 98 | for char in data: 99 | x = (x + 1) % 256 100 | y = (y + box[x]) % 256 101 | box[x], box[y] = box[y], box[x] 102 | out.append(chr(ord(char) ^ box[(box[x] + box[y]) % 256])) 103 | 104 | return ''.join(out) 105 | 106 | def get_config_data_64(self, data, pe): 107 | for pattern in CONFIG_PATTERNS: 108 | m = re.search(pattern, data) 109 | if m: 110 | #print("found pattern") 111 | rva_offset_config = pe.get_rva_from_offset(m.start(2)) + unpack(" len(data[2:]): 132 | print("[!] invalid length") 133 | return "" 134 | data = data[2:2 + length] 135 | tmp = "" 136 | for i, c in enumerate(data): 137 | val = (((i >> 5) + (i << 7) + length + ~i) & 0xFF) 138 | tmp += chr(ord(c) ^ (((i >> 5) + (i << 7) + length + ~i) & 0xFF)) 139 | 140 | tmp = map(ord, list(tmp))[1:] 141 | i = 0 142 | block_len = 16 143 | dec = "" 144 | try: 145 | while i < len(tmp): 146 | if block_len == 16: 147 | block_flag = (tmp[i] << 8) + tmp[i + 1] 148 | block_len = 0 149 | i += 2 150 | 151 | if block_flag & (0x8000 >> block_len) != 0: 152 | char_flag = (tmp[i + 1] >> 4) + (16 * tmp[i]) 153 | if char_flag != 0: 154 | loop_count = (tmp[i + 1] & 0xF) + 3 155 | for n in range(loop_count): 156 | dec += dec[-char_flag] 157 | i += 2 158 | else: 159 | loop_count = (tmp[i + 1] << 8) + tmp[i + 2] + 16 160 | for n in range(loop_count): 161 | #data += chr(tmp[i + 3]) 162 | pass 163 | i += 4 164 | else: 165 | dec += chr(tmp[i]) 166 | i += 1 167 | 168 | block_len += 1 169 | except: 170 | raise 171 | return "" 172 | return dec 173 | 174 | def decrypt(self, dec): 175 | decrypted_len = len(dec) 176 | decomp = [] 177 | processed_len = 0 178 | while (decrypted_len > processed_len): 179 | enc_compressed_len = unpack(" len(enc_compressed): 182 | break 183 | processed_len += enc_compressed_len + 2 184 | tmp = [] 185 | for i in range(enc_compressed_len): 186 | xor_key = (i >> 5) & 0xff 187 | xor_key += (i << 7) & 0xff 188 | xor_key += enc_compressed_len 189 | xor_key += ~i 190 | xor_key = xor_key & 0xff 191 | tmp.append(chr(ord(enc_compressed[i]) ^ xor_key)) 192 | compressed = "".join(tmp) 193 | decompressed = self.decompress(compressed) 194 | decomp.append(decompressed) 195 | return "".join(decomp) 196 | 197 | def calculate(self): 198 | 199 | if not has_yara: 200 | debug.error("Yara must be installed for this plugin") 201 | 202 | addr_space = utils.load_as(self._config) 203 | 204 | os, memory_model = self.is_valid_profile(addr_space.profile) 205 | if not os: 206 | debug.error("This command does not support the selected profile.") 207 | 208 | rules = yara.compile(sources=datper_sig) 209 | 210 | for task in self.filter_tasks(tasks.pslist(addr_space)): 211 | scanner = malfind.VadYaraScanner(task=task, rules=rules) 212 | 213 | for hit, address in scanner.scan(): 214 | 215 | vad_base_addr, end = self.get_vad_base(task, address) 216 | proc_addr_space = task.get_process_address_space() 217 | data = proc_addr_space.zread(vad_base_addr, end - vad_base_addr) 218 | 219 | config_data = [] 220 | 221 | try: 222 | pe = pefile.PE(data=data) 223 | except: 224 | # print("[!] could not parse as a PE file") 225 | break 226 | 227 | config_size = CONFSIZE 228 | 229 | if pe.FILE_HEADER.Machine in (pefile.MACHINE_TYPE['IMAGE_FILE_MACHINE_IA64'], pefile.MACHINE_TYPE['IMAGE_FILE_MACHINE_AMD64']): 230 | enc = self.get_config_data_64(data, pe) 231 | else: 232 | enc = self.get_config_data_32(data, pe, vad_base_addr) 233 | 234 | dec = "" 235 | for key in RC4KEY: 236 | for rc4key_seed in range(0xFF): 237 | dec = self.custom_rc4(enc, key, rc4key_seed) 238 | dec = self.decrypt(dec) 239 | for dline in config_delimiter: 240 | if dline in dec: 241 | break 242 | else: 243 | continue 244 | break 245 | else: 246 | continue 247 | break 248 | 249 | if dec == "": 250 | dec = self.decrypt(enc) 251 | for dline in config_delimiter: 252 | if dline in dec: 253 | key = "NULL" 254 | rc4key_seed = "NULL" 255 | break 256 | 257 | p_data = OrderedDict() 258 | if dec != "": 259 | p_data["RC4 key"] = key 260 | p_data["RC4 Sbox seed"] = rc4key_seed 261 | p_data["Config delimiter"] = dline 262 | idx = 0 263 | for e in (dec.split(dline)): 264 | try: 265 | p_data[idx_list[idx]] = e 266 | except: 267 | p_data["Unknown " + str(idx)] = e 268 | idx += 1 269 | else: 270 | outfd.write("[!] failed to decrypt\n") 271 | 272 | config_data.append(p_data) 273 | 274 | yield task, vad_base_addr, end, hit, memory_model, config_data 275 | break 276 | 277 | def render_text(self, outfd, data): 278 | 279 | delim = '-' * 70 280 | 281 | for task, start, end, malname, memory_model, config_data in data: 282 | outfd.write("{0}\n".format(delim)) 283 | outfd.write("Process: {0} ({1})\n\n".format(task.ImageFileName, task.UniqueProcessId)) 284 | 285 | outfd.write("[Config Info]\n") 286 | for p_data in config_data: 287 | for id, param in p_data.items(): 288 | outfd.write("{0:<16}: {1}\n".format(id, param)) 289 | -------------------------------------------------------------------------------- /utils/aplib.py: -------------------------------------------------------------------------------- 1 | # this is a standalone single-file merge of aplib compression and decompression 2 | # taken from my own library Kabopan http://code.google.com/p/kabopan/ 3 | # (no other clean-up or improvement) 4 | 5 | # Ange Albertini, BSD Licence, 2007-2011 6 | 7 | # from kbp\comp\_lz77.py ################################################## 8 | def find_longest_match(s, sub): 9 | """returns the number of byte to look backward and the length of byte to copy)""" 10 | if sub == "": 11 | return 0, 0 12 | limit = len(s) 13 | dic = s[:] 14 | l = 0 15 | offset = 0 16 | length = 0 17 | first = 0 18 | word = "" 19 | 20 | word += sub[l] 21 | pos = dic.rfind(word, 0, limit + 1) 22 | if pos == -1: 23 | return offset, length 24 | 25 | offset = limit - pos 26 | length = len(word) 27 | dic += sub[l] 28 | 29 | while l < len(sub) - 1: 30 | l += 1 31 | word += sub[l] 32 | 33 | pos = dic.rfind(word, 0, limit + 1) 34 | if pos == -1: 35 | return offset, length 36 | offset = limit - pos 37 | length = len(word) 38 | dic += sub[l] 39 | return offset, length 40 | 41 | # from _misc.py ############################### 42 | 43 | def int2lebin(value, size): 44 | """ouputs value in binary, as little-endian""" 45 | result = "" 46 | for i in xrange(size): 47 | result = result + chr((value >> (8 * i)) & 0xFF ) 48 | return result 49 | 50 | def modifystring(s, sub, offset): 51 | """overwrites 'sub' at 'offset' of 's'""" 52 | return s[:offset] + sub + s[offset + len(sub):] 53 | 54 | def getbinlen(value): 55 | """return the bit length of an integer""" 56 | result = 0 57 | if value == 0: 58 | return 1 59 | while value != 0: 60 | value >>= 1 61 | result += 1 62 | return result 63 | 64 | # from kbp\_bits.py ################################# 65 | class _bits_compress(): 66 | """bit machine for variable-sized auto-reloading tag compression""" 67 | def __init__(self, tagsize): 68 | """tagsize is the number of bytes that takes the tag""" 69 | self.out = "" 70 | 71 | self.__tagsize = tagsize 72 | self.__tag = 0 73 | self.__tagoffset = -1 74 | self.__maxbit = (self.__tagsize * 8) - 1 75 | self.__curbit = 0 76 | self.__isfirsttag = True 77 | 78 | 79 | def getdata(self): 80 | """builds an output string of what's currently compressed: 81 | currently output bit + current tag content""" 82 | tagstr = int2lebin(self.__tag, self.__tagsize) 83 | return modifystring(self.out, tagstr, self.__tagoffset) 84 | 85 | def write_bit(self, value): 86 | """writes a bit, make space for the tag if necessary""" 87 | if self.__curbit != 0: 88 | self.__curbit -= 1 89 | else: 90 | if self.__isfirsttag: 91 | self.__isfirsttag = False 92 | else: 93 | self.out = self.getdata() 94 | self.__tagoffset = len(self.out) 95 | self.out += "".join(["\x00"] * self.__tagsize) 96 | self.__curbit = self.__maxbit 97 | self.__tag = 0 98 | 99 | if value: 100 | self.__tag |= (1 << self.__curbit) 101 | return 102 | 103 | def write_bitstring(self, s): 104 | """write a string of bits""" 105 | for c in s: 106 | self.write_bit(0 if c == "0" else 1) 107 | return 108 | 109 | def write_byte(self, b): 110 | """writes a char or a number""" 111 | assert len(b) == 1 if isinstance(b, str) else 0 <= b <= 255 112 | self.out += b[0:1] if isinstance(b, str) else chr(b) 113 | return 114 | 115 | def write_fixednumber(self, value, nbbit): 116 | """write a value on a fixed range of bits""" 117 | for i in xrange(nbbit - 1, -1, -1): 118 | self.write_bit( (value >> i) & 1) 119 | return 120 | 121 | def write_variablenumber(self, value): 122 | assert value >= 2 123 | 124 | length = getbinlen(value) - 2 # the highest bit is 1 125 | self.write_bit(value & (1 << length)) 126 | for i in xrange(length - 1, -1, -1): 127 | self.write_bit(1) 128 | self.write_bit(value & (1 << i)) 129 | self.write_bit(0) 130 | return 131 | 132 | class _bits_decompress(): 133 | """bit machine for variable-sized auto-reloading tag decompression""" 134 | def __init__(self, data, tagsize): 135 | self.__curbit = 0 136 | self.__offset = 0 137 | self.__tag = None 138 | self.__tagsize = tagsize 139 | self.__in = data 140 | self.out = "" 141 | 142 | def getoffset(self): 143 | """return the current byte offset""" 144 | return self.__offset 145 | 146 | # def getdata(self): 147 | # return self.__lzdata 148 | 149 | def read_bit(self): 150 | """read next bit from the stream, reloads the tag if necessary""" 151 | if self.__curbit != 0: 152 | self.__curbit -= 1 153 | else: 154 | self.__curbit = (self.__tagsize * 8) - 1 155 | self.__tag = ord(self.read_byte()) 156 | for i in xrange(self.__tagsize - 1): 157 | self.__tag += ord(self.read_byte()) << (8 * (i + 1)) 158 | 159 | bit = (self.__tag >> ((self.__tagsize * 8) - 1)) & 0x01 160 | self.__tag <<= 1 161 | return bit 162 | 163 | def is_end(self): 164 | return self.__offset == len(self.__in) and self.__curbit == 1 165 | 166 | def read_byte(self): 167 | """read next byte from the stream""" 168 | if type(self.__in) == str: 169 | result = self.__in[self.__offset] 170 | elif type(self.__in) == file: 171 | result = self.__in.read(1) 172 | self.__offset += 1 173 | return result 174 | 175 | def read_fixednumber(self, nbbit, init=0): 176 | """reads a fixed bit-length number""" 177 | result = init 178 | for i in xrange(nbbit): 179 | result = (result << 1) + self.read_bit() 180 | return result 181 | 182 | def read_variablenumber(self): 183 | """return a variable bit-length number x, x >= 2 184 | 185 | reads a bit until the next bit in the pair is not set""" 186 | result = 1 187 | result = (result << 1) + self.read_bit() 188 | while self.read_bit(): 189 | result = (result << 1) + self.read_bit() 190 | return result 191 | 192 | def read_setbits(self, max_, set_=1): 193 | """read bits as long as their set or a maximum is reached""" 194 | result = 0 195 | while result < max_ and self.read_bit() == set_: 196 | result += 1 197 | return result 198 | 199 | def back_copy(self, offset, length=1): 200 | for i in xrange(length): 201 | self.out += self.out[-offset] 202 | return 203 | 204 | def read_literal(self, value=None): 205 | if value is None: 206 | self.out += self.read_byte() 207 | else: 208 | self.out += value 209 | return False 210 | 211 | # from kbp\comp\aplib.py ################################################### 212 | """ 213 | aPLib, LZSS based lossless compression algorithm 214 | 215 | Jorgen Ibsen U{http://www.ibsensoftware.com} 216 | """ 217 | 218 | def lengthdelta(offset): 219 | if offset < 0x80 or 0x7D00 <= offset: 220 | return 2 221 | elif 0x500 <= offset: 222 | return 1 223 | return 0 224 | 225 | class compress(_bits_compress): 226 | """ 227 | aplib compression is based on lz77 228 | """ 229 | def __init__(self, data, length=None): 230 | _bits_compress.__init__(self, 1) 231 | self.__in = data 232 | self.__length = length if length is not None else len(data) 233 | self.__offset = 0 234 | self.__lastoffset = 0 235 | self.__pair = True 236 | return 237 | 238 | def __literal(self, marker=True): 239 | if marker: 240 | self.write_bit(0) 241 | self.write_byte(self.__in[self.__offset]) 242 | self.__offset += 1 243 | self.__pair = True 244 | return 245 | 246 | def __block(self, offset, length): 247 | assert offset >= 2 248 | self.write_bitstring("10") 249 | 250 | # if the last operations were literal or single byte 251 | # and the offset is unchanged since the last block copy 252 | # we can just store a 'null' offset and the length 253 | if self.__pair and self.__lastoffset == offset: 254 | self.write_variablenumber(2) # 2- 255 | self.write_variablenumber(length) 256 | else: 257 | high = (offset >> 8) + 2 258 | if self.__pair: 259 | high += 1 260 | self.write_variablenumber(high) 261 | low = offset & 0xFF 262 | self.write_byte(low) 263 | self.write_variablenumber(length - lengthdelta(offset)) 264 | self.__offset += length 265 | self.__lastoffset = offset 266 | self.__pair = False 267 | return 268 | 269 | def __shortblock(self, offset, length): 270 | assert 2 <= length <= 3 271 | assert 0 < offset <= 127 272 | self.write_bitstring("110") 273 | b = (offset << 1 ) + (length - 2) 274 | self.write_byte(b) 275 | self.__offset += length 276 | self.__lastoffset = offset 277 | self.__pair = False 278 | return 279 | 280 | def __singlebyte(self, offset): 281 | assert 0 <= offset < 16 282 | self.write_bitstring("111") 283 | self.write_fixednumber(offset, 4) 284 | self.__offset += 1 285 | self.__pair = True 286 | return 287 | 288 | def __end(self): 289 | self.write_bitstring("110") 290 | self.write_byte(chr(0)) 291 | return 292 | 293 | def do(self): 294 | self.__literal(False) 295 | while self.__offset < self.__length: 296 | offset, length = find_longest_match(self.__in[:self.__offset], 297 | self.__in[self.__offset:]) 298 | if length == 0: 299 | c = self.__in[self.__offset] 300 | if c == "\x00": 301 | self.__singlebyte(0) 302 | else: 303 | self.__literal() 304 | elif length == 1 and 0 <= offset < 16: 305 | self.__singlebyte(offset) 306 | elif 2 <= length <= 3 and 0 < offset <= 127: 307 | self.__shortblock(offset, length) 308 | elif 3 <= length and 2 <= offset: 309 | self.__block(offset, length) 310 | else: 311 | self.__literal() 312 | #raise ValueError("no parsing found", offset, length) 313 | self.__end() 314 | return self.getdata() 315 | 316 | 317 | class decompress(_bits_decompress): 318 | def __init__(self, data): 319 | _bits_decompress.__init__(self, data, tagsize=1) 320 | self.__pair = True # paired sequence 321 | self.__lastoffset = 0 322 | self.__functions = [ 323 | self.__literal, 324 | self.__block, 325 | self.__shortblock, 326 | self.__singlebyte] 327 | return 328 | 329 | def __literal(self): 330 | self.read_literal() 331 | self.__pair = True 332 | return False 333 | 334 | def __block(self): 335 | b = self.read_variablenumber() # 2- 336 | if b == 2 and self.__pair : # reuse the same offset 337 | offset = self.__lastoffset 338 | length = self.read_variablenumber() # 2- 339 | else: 340 | high = b - 2 # 0- 341 | if self.__pair: 342 | high -= 1 343 | offset = (high << 8) + ord(self.read_byte()) 344 | length = self.read_variablenumber() # 2- 345 | length += lengthdelta(offset) 346 | self.__lastoffset = offset 347 | self.back_copy(offset, length) 348 | self.__pair = False 349 | return False 350 | 351 | def __shortblock(self): 352 | b = ord(self.read_byte()) 353 | if b <= 1: # likely 0 354 | return True 355 | length = 2 + (b & 0x01) # 2-3 356 | offset = b >> 1 # 1-127 357 | self.back_copy(offset, length) 358 | self.__lastoffset = offset 359 | self.__pair = False 360 | return False 361 | 362 | def __singlebyte(self): 363 | offset = self.read_fixednumber(4) # 0-15 364 | if offset: 365 | self.back_copy(offset) 366 | else: 367 | self.read_literal('\x00') 368 | self.__pair = True 369 | return False 370 | 371 | def do(self): 372 | """returns decompressed buffer and consumed bytes counter""" 373 | self.read_literal() 374 | while True: 375 | if self.__functions[self.read_setbits(3)](): 376 | break 377 | return self.out, self.getoffset() 378 | 379 | if __name__ == "__main__": 380 | # from kbp\test\aplib_test.py ###################################################################### 381 | assert decompress(compress("a").do()).do() == ("a", 3) 382 | assert decompress(compress("ababababababab").do()).do() == ('ababababababab', 9) 383 | assert decompress(compress("aaaaaaaaaaaaaacaaaaaa").do()).do() == ('aaaaaaaaaaaaaacaaaaaa', 11) 384 | 385 | --------------------------------------------------------------------------------