-
Notifications
You must be signed in to change notification settings - Fork 3
/
cave-explorer.py
169 lines (120 loc) · 5.36 KB
/
cave-explorer.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
"""
Jean-Pierre LESUEUR (@DarkCoderSc)
https://www.phrozen.io/
Requirement:
- pip install pefile
- pip install colorama
"""
import argparse
import logging
import os
import re
import struct
import sys
import pefile
from colorama import Fore, Style, init
init()
class ColoredBackticksFormatter(logging.Formatter):
def format(self, record):
record.msg = self.highlight_backticks(record.msg)
return super().format(record)
def highlight_backticks(self, message):
pattern = r'`([^`]+)`'
colored_text = re.sub(pattern, lambda match: f"{Fore.GREEN}`{match.group(1)}`{Style.RESET_ALL}", message)
return colored_text
def get_logger():
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
formatter = ColoredBackticksFormatter("%(levelname)s - %(message)s")
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logger.addHandler(handler)
return logger
def bytearr_to_bytestr(data):
return ''.join(f"\\x{'{:02x}'.format(x)}" for x in data)
def bytestr_to_bytearr(data):
return list(bytearray.fromhex(data.replace("\\x", " ")))
class CodeCave:
def __init__(self, section, offset, size, cave_type):
self.section = section
self.offset = offset
self.size = size
self.type = cave_type
def get_section_name(section):
if not section:
return ""
return section.Name.decode("utf-8").strip('\0').lower()
def code_cave_finder(section, cave_opcode):
code_caves = []
offset = section.VirtualAddress
section_data = pe.get_memory_mapped_image()[offset:offset + section.SizeOfRawData]
cave_length = 0
for index, b in enumerate(section_data, start=1):
if (b == cave_opcode):
cave_length += 1
if ((b != cave_opcode) and (cave_length > 0)) or (index == len(section_data)):
if cave_length >= argv.cave_min_size:
code_caves.append(CodeCave(section, (index - cave_length), cave_length, cave_opcode))
cave_length = 0
return code_caves
def hex_or_int(value):
try:
return int(value)
except ValueError:
try:
return int(value, 16)
except ValueError:
raise argparse.ArgumentTypeError(f"Invalid value: `{value}`. Supported values are integer or hexadecimal integer representation.")
if __name__ == "__main__":
logger = get_logger()
try:
argument_parser = argparse.ArgumentParser(description=f"Cave Explorer (Code Cave Finder for PE executables (x86-32))")
argument_parser.add_argument('-f', '--file', type=str, dest="file", action="store", required=True, help="x86-32 PE Input File.")
argument_parser.add_argument('-c', '--cave-opcodes', type=str, dest="cave_opcodes", action="store", default="\\x00\\x90", help="OpCode considered as valid code caves (Example: NULL(0x00), NOP(0x90)).")
argument_parser.add_argument('-b', '--imagebase', type=hex_or_int, dest="imagebase", action="store", default=None, help="Define a custom imagebase. Default is the one pecified imagebase by file PE Header.")
argument_parser.add_argument('-s', '--cave-min-size', type=int, dest="cave_min_size", action="store", default=30, help="Minimum serie of opcodes to be considered as a cave (in bytes).")
argument_parser.add_argument('-v', '--verbose', type=bool, dest="verbose", action=argparse.BooleanOptionalAction, default=False, help="Increase verbosity.")
try:
argv = argument_parser.parse_args()
except IOError as e:
argument_parser.error()
try:
cave_opcode = bytestr_to_bytearr(argv.cave_opcodes)
except:
raise Exception("Malformed byte string. A byte string must be defined with the following format: \"\\x01\\x02\\x03...\\x0a\".")
pe = pefile.PE(argv.file, fast_load=False)
if pe.FILE_HEADER.Machine != pefile.MACHINE_TYPE["IMAGE_FILE_MACHINE_I386"]:
raise Exception("This script is not compatible with x86-64 PE Files.")
if argv.imagebase is None:
imagebase = pe.OPTIONAL_HEADER.ImageBase
else:
imagebase = argv.imagebase
logger.info("Target file: `{}`".format(argv.file))
logger.info("Working with ImageBase->`{}`".format(format(imagebase, "08X")))
logger.debug("Exploring possible code caves...")
for section in pe.sections:
section_name = get_section_name(section)
if section.Characteristics & 0x20000000 == 0x20000000:
logger.info("Scanning section `{}`, VA->`{}`, PointerToRawData->`{}`, Size->`{}`".format(
section_name,
format(section.VirtualAddress, "08X"),
format(section.PointerToRawData, "08X"),
format(section.SizeOfRawData, "08X"),
))
for opcode in cave_opcode:
code_caves = code_cave_finder(section, opcode)
for index, code_cave in enumerate(code_caves):
logger.info("Cave:{} - Offset->`{}`, RVA->`{}`, Cave Size->`{}` bytes, OpCode->`{}`".format(
index+1,
format(code_cave.offset, "08X"),
format(imagebase + code_cave.section.VirtualAddress + code_cave.offset, "08X"),
code_cave.size,
format(code_cave.type, "02X"),
))
else:
if argv.verbose:
logger.warning("Section `{}` is not executable, skipping...".format(section_name))
logger.info("Done.")
except Exception as e:
exc_type, exc_obj, exc_tb = sys.exc_info()
logger.error(f"{str(e)}, line=[{exc_tb.tb_lineno}]")