├── 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 | [](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 | 
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 | [](https://youtu.be/n36WAzgHldY)
72 |
73 | And, following [YouTube video](https://youtu.be/kPsOvoRHK3k) is the demonstration of MalConfScan.
74 |
75 | [](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 |
--------------------------------------------------------------------------------