├── README.md └── hollowfind.py /README.md: -------------------------------------------------------------------------------- 1 | # HollowFind 2 | Hollowfind is a Volatility plugin to detect different types of process hollowing techniques used in the wild to bypass, confuse, deflect and divert the forensic analysis techniques. The plugin detects such attacks by finding discrepancy in the VAD and PEB, it also disassembles the address of entry point to detect any redirection attempts and also reports any suspicious memory regions which should help in detecting any injected code. 3 | 4 | Full details can be found in the link: 5 | https://cysinfo.com/detecting-deceptive-hollowing-techniques/ 6 | 7 | ### Running the Plugin 8 | 9 | Copy the plugin to volatility/plugins directory 10 | 11 | Run the plugin against memory image as shown below 12 | 13 | ```sh 14 | $ python vol.py -f infected.vmem --profile=Win7SP0x86 hollowfind 15 | ``` 16 | 17 | ### Other Options 18 | 19 | To filter on a specic process id use -p option 20 | 21 | ```sh 22 | $ python vol.py -f infected.vmem --profile=Win7SP0x86 hollowfind -p 1820 23 | ``` 24 | 25 | To filter on multiple process ids use -p followed by comma seperated process ids 26 | 27 | ```sh 28 | $ python vol.py -f infected.vmem --profile=Win7SP0x86 hollowfind -p 1820,868 29 | ``` 30 | 31 | To dump the suspicious memory regions use -D followed by directory name 32 | 33 | ```sh 34 | $ python vol.py -f infected.vmem --profile=Win7SP0x86 hollowfind -p 1820 -D dump/ 35 | ``` 36 | 37 | To redirect the output to file use --output-file option 38 | 39 | ```sh 40 | $ python vol.py -f infected.vmem --profile=Win7SP0x86 hollowfind --output-file=output.txt 41 | ``` 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /hollowfind.py: -------------------------------------------------------------------------------- 1 | # Author: Monnappa K A 2 | # Email : monnappa22@gmail.com 3 | # Twitter: @monnappa22 4 | # Description: Volatility plugin to detect different types of Process Hollowing 5 | 6 | import os 7 | import volatility.obj as obj 8 | import volatility.utils as utils 9 | from volatility.plugins.taskmods import PSList 10 | import volatility.plugins.vadinfo as vadinfo 11 | import volatility.plugins.malware.malfind as malfind 12 | from volatility.renderers.basic import Address,Hex 13 | 14 | 15 | hollow_types = dict(enumerate(["Invalid EXE Memory Protection and Process Path Discrepancy", 16 | "No VAD Entry For Process Executable", 17 | "Process Base Address and Memory Protection Discrepancy"])) 18 | 19 | class HollowFind(vadinfo.VADDump): 20 | """Detects different types of Process Hollowing""" 21 | 22 | def __init__(self, config, *args, **kwargs): 23 | vadinfo.VADDump.__init__(self, config, *args, **kwargs) 24 | config.remove_option("BASE") 25 | 26 | def update_proc_peb_info(self, psdata): 27 | self.proc_peb_info = {} 28 | # Builds a dictionary of process executable information from PEB 29 | for proc in psdata: 30 | pid = int(proc.UniqueProcessId) 31 | self.proc_peb_info[pid] = [proc, 32 | pid, 33 | proc.ImageFileName, 34 | int(proc.InheritedFromUniqueProcessId), 35 | str(proc.CreateTime)] 36 | if proc.Peb: 37 | # gets process information for the process executable from PEB and updates the dictionary 38 | mods = proc.get_load_modules() 39 | for mod in mods: 40 | ext = os.path.splitext(str(mod.FullDllName))[1].lower() 41 | if (ext == ".exe"): 42 | proc_cmd_line = proc.Peb.ProcessParameters.CommandLine 43 | proc_image_baseaddr = proc.Peb.ImageBaseAddress 44 | mod_baseaddr = mod.DllBase 45 | mod_size = mod.SizeOfImage 46 | mod_basename = mod.BaseDllName 47 | mod_fullname = mod.FullDllName 48 | break 49 | 50 | self.proc_peb_info[pid].extend([str(proc_cmd_line), 51 | Address(proc_image_baseaddr), 52 | Address(mod_baseaddr), 53 | Hex(mod_size), 54 | str(mod_basename), 55 | str(mod_fullname or "")]) 56 | 57 | else: 58 | self.proc_peb_info[pid].extend(["No PEB", Address(0), Address(0), Hex(0), "No PEB", "No PEB"]) 59 | 60 | 61 | def update_proc_vad_info(self, proc_peb_info): 62 | """Builds a dictionary of process executable information from VAD""" 63 | self.proc_vad_info = {} 64 | for pid in proc_peb_info: 65 | self.proc_vad_info[pid] = [] 66 | proc = proc_peb_info[pid][0] 67 | 68 | if proc.Peb: 69 | # gets process information for the process executable from VAD and updates the dictionary 70 | for vad, addr_space in proc.get_vads(vad_filter = proc._mapped_file_filter): 71 | ext = "" 72 | vad_found = False 73 | if obj.Object("_IMAGE_DOS_HEADER", offset = vad.Start, vm = addr_space).e_magic != 0x5A4D: 74 | continue 75 | 76 | if str(vad.FileObject.FileName or ''): 77 | ext = os.path.splitext(str(vad.FileObject.FileName))[1].lower() 78 | 79 | if (ext == ".exe") or (vad.Start == proc.Peb.ImageBaseAddress): 80 | vad_filename = vad.FileObject.FileName 81 | vad_baseaddr = vad.Start 82 | vad_size = vad.End - vad.Start 83 | vad_protection = vadinfo.PROTECT_FLAGS.get(vad.VadFlags.Protection.v()) 84 | vad_tag = vad.Tag 85 | self.proc_vad_info[pid].extend([str(vad_filename or ''), 86 | Address(vad_baseaddr), 87 | Hex(vad_size), 88 | str(vad_protection or ''), 89 | str(vad_tag or '')]) 90 | vad_found = True 91 | break 92 | 93 | if vad_found == False: 94 | self.proc_vad_info[pid].extend(["NA", Address(0), Hex(0), "NA", "NA"]) 95 | 96 | else: 97 | self.proc_vad_info[pid].extend(["No VAD", Address(0), Hex(0), "No VAD", "No VAD"]) 98 | 99 | def get_proc_peb_info(self): 100 | return self.proc_peb_info 101 | 102 | def get_proc_vad_info(self): 103 | return self.proc_vad_info 104 | 105 | def detect_proc_hollow(self): 106 | """Detects hollowed processes and returns dictionary with pid as the key and type of process hollowing as value""" 107 | proc_peb_info = self.get_proc_peb_info() 108 | proc_vad_info = self.get_proc_vad_info() 109 | hol_type = None 110 | self.hollowed_procs = {} 111 | for pid in proc_peb_info: 112 | (proc, pid, proc_name, ppid, create_time, proc_cmd_line, proc_image_baseaddr, mod_baseaddr, 113 | mod_size, mod_basename, mod_fullname) = proc_peb_info[pid] 114 | (vad_filename, vad_baseaddr, vad_size, vad_protection, vad_tag) = proc_vad_info[pid] 115 | 116 | if vad_protection == "PAGE_EXECUTE_READWRITE": 117 | hol_type = 0 118 | self.hollowed_procs[pid] = hol_type 119 | 120 | elif vad_protection == "NA": 121 | hol_type = 1 122 | self.hollowed_procs[pid] = hol_type 123 | 124 | elif (vad_protection == "PAGE_EXECUTE_WRITECOPY") and (vad_baseaddr != proc_image_baseaddr): 125 | hol_type = 2 126 | self.hollowed_procs[pid] = hol_type 127 | return self.hollowed_procs 128 | 129 | 130 | def update_parent_proc_info(self, proc_peb_info): 131 | """Builds a dictionary containing parent process information for all the processes""" 132 | self.parent_proc_info = {} 133 | for pid in proc_peb_info: 134 | self.parent_proc_info[pid] = [] 135 | if pid == 4: 136 | self.parent_proc_info[pid].extend(["", 0]) 137 | else: 138 | ppid = int(proc_peb_info[pid][3]) 139 | if ppid in proc_peb_info: 140 | ppname = str(proc_peb_info[ppid][2]) 141 | else: 142 | ppname = "NA" 143 | 144 | self.parent_proc_info[pid].extend([ppname, ppid]) 145 | 146 | def get_parent_proc_info(self): 147 | return self.parent_proc_info 148 | 149 | def get_similar_procs(self, procid): 150 | """Given a process id returns a list containing information of similar processes""" 151 | self.similar_procs = [] 152 | proc_peb_info = self.get_proc_peb_info() 153 | parent_proc_info = self.get_parent_proc_info() 154 | pname = proc_peb_info[procid][2] 155 | create_time = proc_peb_info[procid][4] 156 | ppname, ppid = parent_proc_info[procid] 157 | self.similar_procs.append([pname, procid, ppname, ppid, create_time]) 158 | 159 | for pid in proc_peb_info: 160 | if pid == procid: 161 | continue 162 | if pname == proc_peb_info[pid][2]: 163 | proc_name = proc_peb_info[pid][2] 164 | creation_time = proc_peb_info[pid][4] 165 | parent_name, parent_id = parent_proc_info[pid] 166 | self.similar_procs.append([proc_name, pid, parent_name, parent_id, creation_time]) 167 | return self.similar_procs 168 | 169 | 170 | def calculate(self): 171 | if self._config.PID: 172 | filter_pid = self._config.PID 173 | # This is so that when -p option is given it can still enumerate all processes to determine similar processes 174 | self._config.PID = None 175 | else: 176 | filter_pid = None 177 | ps = PSList(self._config) 178 | psdata = ps.calculate() 179 | self.update_proc_peb_info(psdata) 180 | proc_peb_info = self.get_proc_peb_info() 181 | self.update_parent_proc_info(proc_peb_info) 182 | parent_proc_info = self.get_parent_proc_info() 183 | self.update_proc_vad_info(proc_peb_info) 184 | hol_procs = self.detect_proc_hollow() 185 | proc_vad_info = self.get_proc_vad_info() 186 | if hol_procs: 187 | for (hol_pid, hol_type) in hol_procs.items(): 188 | similar_procs = self.get_similar_procs(hol_pid) 189 | if not filter_pid: 190 | yield (proc_peb_info[hol_pid], 191 | proc_vad_info[hol_pid], 192 | hol_pid, hol_type, 193 | similar_procs, 194 | parent_proc_info[hol_pid] ) 195 | else: 196 | for p in filter_pid.split(','): 197 | fil_pid = int(p) 198 | if int(fil_pid) == hol_pid: 199 | yield (proc_peb_info[hol_pid], 200 | proc_vad_info[hol_pid], 201 | hol_pid, hol_type, 202 | similar_procs, 203 | parent_proc_info[hol_pid]) 204 | 205 | def render_text(self, outfd, data): 206 | for (hol_proc_peb_info, hol_proc_vad_info, hol_pid, hol_type, similar_procs, parent_proc_info) in data: 207 | (proc, pid, proc_name, ppid, create_time, proc_cmd_line, proc_image_baseaddr, mod_baseaddr, 208 | mod_size, mod_basename, mod_fullname) = hol_proc_peb_info 209 | (vad_filename, vad_baseaddr, vad_size, vad_protection, vad_tag) = hol_proc_vad_info 210 | (parent_name, parent_id) = parent_proc_info 211 | 212 | outfd.write("Hollowed Process Information:\n") 213 | outfd.write("\tProcess: {0} PID: {1}\n".format(proc_name, hol_pid)) 214 | outfd.write("\tParent Process: {0} PPID: {1}\n".format(parent_name, ppid)) 215 | outfd.write("\tCreation Time: {0}\n".format(create_time)) 216 | outfd.write("\tProcess Base Name(PEB): {0}\n".format(mod_basename)) 217 | outfd.write("\tCommand Line(PEB): {0}\n".format(proc_cmd_line)) 218 | outfd.write("\tHollow Type: {0}\n".format(hollow_types[hol_type])) 219 | outfd.write("\n") 220 | outfd.write( "VAD and PEB Comparison:\n") 221 | outfd.write( "\tBase Address(VAD): {0:#x}\n".format(vad_baseaddr)) 222 | outfd.write( "\tProcess Path(VAD): {0}\n".format(vad_filename)) 223 | outfd.write( "\tVad Protection: {0}\n".format(vad_protection)) 224 | outfd.write( "\tVad Tag: {0}\n".format(vad_tag)) 225 | outfd.write("\n") 226 | 227 | if hol_type == 0: 228 | addr_space = proc.get_process_address_space() 229 | dos_header = obj.Object("_IMAGE_DOS_HEADER", offset=proc_image_baseaddr, vm=addr_space) 230 | nt_header = dos_header.get_nt_header() 231 | optional_header = obj.Object("_IMAGE_OPTIONAL_HEADER", offset=nt_header.obj_offset+0x18, vm=addr_space) 232 | ep_addr = proc_image_baseaddr + optional_header.AddressOfEntryPoint 233 | content = addr_space.read(ep_addr, 64) 234 | outfd.write("\tBase Address(PEB): {0:#x}\n".format(proc_image_baseaddr)) 235 | outfd.write("\tProcess Path(PEB): {0}\n" .format(mod_fullname)) 236 | outfd.write("\tMemory Protection: {0}\n".format(vad_protection)) 237 | outfd.write("\tMemory Tag: {0}\n".format(vad_tag)) 238 | outfd.write("\n") 239 | outfd.write("Disassembly(Entry Point):\n") 240 | if content != None: 241 | outfd.write("\n".join(["\t{0:#010x} {1:<16} {2}".format(o, h, i) 242 | for o, i, h in malfind.Disassemble(content, ep_addr) 243 | ])) 244 | else: 245 | outfd.write("\tNo Disassembly: Memory Unreadable at {0:#010x}\n".format(ep_addr)) 246 | 247 | outfd.write("\n\n") 248 | 249 | if (hol_type == 1) or (hol_type == 2): 250 | for vad, addr_space in proc.get_vads(): 251 | if vad.Start == proc_image_baseaddr: 252 | content = addr_space.read(vad.Start, 64) 253 | outfd.write("\tBase Address(PEB): {0:#x}\n".format(proc_image_baseaddr)) 254 | outfd.write("\tProcess Path(PEB): {0}\n" .format(mod_fullname)) 255 | outfd.write("\tMemory Protection: {0}\n".format(str(vadinfo.PROTECT_FLAGS.get(vad.VadFlags.Protection.v()) or ""))) 256 | outfd.write("\tMemory Tag: {0}\n".format(str(vad.Tag or ""))) 257 | outfd.write("\n") 258 | if content != None: 259 | outfd.write("".join(["{0:#010x} {1:<48} {2}\n".format(vad.Start + o, h, ''.join(c)) 260 | for o, h, c in utils.Hexdump(content)])) 261 | else: 262 | outfd.write("\tNo Hexdump: Memory Unreadable at {0:#010x}\n".format(vad.Start)) 263 | outfd.write("\n") 264 | 265 | outfd.write("Similar Processes:\n") 266 | for similar_proc in similar_procs: 267 | (process_name, process_id, parent_name, parent_id, creation_time) = similar_proc 268 | outfd.write("\t{0}({1}) Parent:{2}({3}) Start:{4}\n".format(process_name, 269 | process_id, 270 | parent_name, 271 | parent_id, 272 | creation_time)) 273 | outfd.write("\n") 274 | 275 | outfd.write("Suspicious Memory Regions:\n") 276 | for vad, addr_space in proc.get_vads(): 277 | content = addr_space.read(vad.Start, 64) 278 | if content == None: 279 | continue 280 | vad_prot = str(vadinfo.PROTECT_FLAGS.get(vad.VadFlags.Protection.v())) 281 | if obj.Object("_IMAGE_DOS_HEADER", offset = vad.Start, vm = addr_space).e_magic != 0x5A4D: 282 | flag = "No PE/Possibly Code" 283 | if (vad_prot == "PAGE_EXECUTE_READWRITE"): 284 | sus_addr = vad.Start 285 | outfd.write("\t{0:#x}({1}) Protection: {2} Tag: {3}\n".format(vad.Start, 286 | flag, 287 | vad_prot, 288 | str(vad.Tag or ""))) 289 | if self._config.DUMP_DIR: 290 | filename = os.path.join(self._config.DUMP_DIR,"process.{0}.{1:#x}.dmp".format(hol_pid, sus_addr)) 291 | self.dump_vad(filename, vad, addr_space) 292 | 293 | elif (vad_prot == "PAGE_EXECUTE_WRITECOPY"): 294 | sus_addr = vad.Start 295 | outfd.write("\t{0:#x}({1}) Protection: {2} Tag: {3}\n".format(sus_addr, 296 | flag, 297 | vad_prot, 298 | str(vad.Tag or ""))) 299 | if self._config.DUMP_DIR: 300 | filename = os.path.join(self._config.DUMP_DIR,"process.{0}.{1:#x}.dmp".format(hol_pid, 301 | sus_addr)) 302 | self.dump_vad(filename, vad, addr_space) 303 | 304 | else: 305 | if vad_prot == "PAGE_EXECUTE_READWRITE": 306 | flag = "PE Found" 307 | sus_addr = vad.Start 308 | outfd.write("\t{0:#x}({1}) Protection: {2} Tag: {3}\n".format(sus_addr, 309 | flag, 310 | vad_prot, 311 | str(vad.Tag or ""))) 312 | if self._config.DUMP_DIR: 313 | filename = os.path.join(self._config.DUMP_DIR,"process.{0}.{1:#x}.dmp".format(hol_pid, 314 | sus_addr)) 315 | self.dump_vad(filename, vad, addr_space) 316 | 317 | elif (vad_prot == "PAGE_EXECUTE_WRITECOPY") and (not bool(vad.FileObject)): 318 | flag = "PE - No Mapped File" 319 | sus_addr = vad.Start 320 | outfd.write("\t{0:#x}({1}) Protection: {2} Tag: {3}\n".format(sus_addr, 321 | flag, 322 | vad_prot, 323 | str(vad.Tag or ""))) 324 | if self._config.DUMP_DIR: 325 | filename = os.path.join(self._config.DUMP_DIR,"process.{0}.{1:#x}.dmp".format(hol_pid, 326 | sus_addr)) 327 | self.dump_vad(filename, vad, addr_space) 328 | 329 | outfd.write("---------------------------------------------------\n\n") 330 | 331 | 332 | 333 | --------------------------------------------------------------------------------