-
Notifications
You must be signed in to change notification settings - Fork 0
/
bbis_hide.py
214 lines (178 loc) · 9.39 KB
/
bbis_hide.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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
"""bbis_hide.py is a python script for hiding data in executables (e.g. .EXE).
This script implements a PoC (Proof-of-Concept) system for BBIS (Binary-Based Instruction Substitution)."""
import os
import argparse
from opcode_map_db import target_map, decode_opcode_map_0_to_1, decode_opcode_map_1_to_0
map_dict = \
{
0: decode_opcode_map_1_to_0,
1: decode_opcode_map_0_to_1
}
def get_arguments_cli():
"""Gets user input from CLI."""
parser = argparse.ArgumentParser(prog='bbis_hide.py',
description='Python script to decode\extract data within executable file.')
# parser.add_argument('-a', '--action', type=str, required=True, help='-a or --action [=decode,=extract] (choose whether you want to decode or extract data)')
parser.add_argument('-d', '--data', type=str, required=True, help='Path to data (file) to hide in executable')
parser.add_argument('-e', '--executable', type=str, required=True, help='Path to executable to hide file within')
parser.add_argument('-o', '--output_path', type=str, required=False, help='Output path for modified executable')
return parser.parse_args()
def get_file_binary_data(file):
"""Reads file's binary data, converts binary data to binary string.
:param file: file path
:return binary: file's binary data in base 2 (binary) as string"""
with open(file, 'rb') as reader:
binary = reader.read()
binary = bin(int.from_bytes(binary, byteorder='big'))[2:]
return binary
def get_executable_binary(executable):
"""Reads executable and returns binary data as bytearray"""
return bytearray(open(executable, 'rb').read())
def get_executable_object_data(executable):
try:
# Get executable name from path
exe_name = os.path.basename(executable).split('.')[0]
# Disassemble .EXE file's code section with intel x8086 mnemonics and save output
os.system(f'objdump -d -M intel {executable} > {exe_name}_dump.txt')
# Get info about object code - code section's offset,size,virtual address
os.system(f'objdump -h {executable} > {exe_name}_code_offset.txt')
# Read object code's disassembly to 'dump'
dump = open(f'{exe_name}_dump.txt', 'r').readlines()[7:]
# Read object code's information to 'info'
info = open(f'{exe_name}_code_offset.txt', 'r').readlines()[5:6]
# Converting 'info' from list to str
info = str(info[0])
virtual_offset = info[28:36]
code_offset = info[48:56]
dump = [x.split('\t') for x in dump]
except Exception:
print(f"Objdump unable to disassemble {executable}")
print("Program exits")
clear_logs(executable)
exit(1)
return (dump, virtual_offset, code_offset)
def organize_mnemonics_list(dump):
# Iterate through 'dump' and append commands to 'mnemonic_list'
# Each x is a list and contains [offset,hex value,command(optional)] (sometimes there's NOP or line of zero)
return [[x[0].replace(' ', ''), x[1].replace(' ', ''), x[2].replace(' ', '')] for x in dump if len(x) == 3]
def get_target_mnemonics(mnemonic_list):
return [mnemonic_list[i] for i in range(len(mnemonic_list)) if mnemonic_list[i][1] in target_map]
def calculate_offsets(target_list, virtual_offset, code_offset):
"""Calculates for every target mnemonic it's real offset from virtual offset and returns a list of offsets with their opcodes"""
offsets = [(int(x[0][:-1],16) - int(virtual_offset,16) + int(code_offset,16), x[1]) for x in target_list]
# For debugging purposes
# with open("offsets-list.txt","w") as file:
# for offset in offsets:
# file.write(f"{offset}\n")
return offsets
# return [(int(hex((int(x[0][:-1], 16) - int(virtual_offset, 16)) + int(code_offset, 16)), 16), x[1]) for x in target_list]
def get_executable_offsets(executable):
"""Get all assembly mnemonics from binary file code section,
parses them and gets assembly mnemonics that are in the opcode mapping with their offsets"""
# Get executable's assembly code (dump), virtual offset, and code offset (code section location)
res = get_executable_object_data(executable)
dump, virtual_offset, code_offset = res[0], res[1], res[2]
# Organize mnemonics
mnemonic_list = organize_mnemonics_list(dump)
# Get target mnemonics from whole assembly code
target_list = get_target_mnemonics(mnemonic_list)
# Return list of tuples (offset,opcode)
return calculate_offsets(target_list, virtual_offset, code_offset)
def get_opcode_conversion(opcode, param):
"""Converts opcodes based on their value in map and 'param'. If 'param' is 0 and opcode value in map is 1 then
it will convert it to it's equal opcode that has value of 0 in map."""
return int(map_dict.get(param).get(opcode).split()[0]), int(map_dict.get(param).get(opcode).split()[1])
def decode_data_within_executable(buffer, binary_data, offsets, exe_name=None):
"""Gets buffer and binary input to decode inside buffer
:param buffer: Buffer for executable file.
:param binary_data: File's binary representation to decode inside executable (buffer).
:param offsets: List of offsets for mnemonics in buffer."""
i = 0
try:
while i < len(binary_data):
# Current bit in binary data
bit = binary_data[i]
# Current offset from offset list
offset = offsets[i]
# Current opcode (all targeted mnemonics are 2 bytes)
opcode = f'{buffer[offset]} {buffer[offset + 1]}'
# print(f"Offset:{offset}; opcode:{opcode}; bit:{bit}; index:{i}")
if bit == '1':
# If current bit is 1 check whether current opcode gives value of 0 in map
if opcode in decode_opcode_map_0_to_1:
# Substitute opcode with another one that equals 1 in map
buffer[offset], buffer[offset + 1] = get_opcode_conversion(opcode, 1)
# print(f"Converted to: {buffer[offset]} {buffer[offset+1]} => 1")
elif bit == '0':
# If current bit is 0 check whether current opcode gives value of 1 in map
if opcode in decode_opcode_map_1_to_0:
# Substitute opcode with another one that equals 0 in map
buffer[offset], buffer[offset + 1] = get_opcode_conversion(opcode, 0)
# print(f"Converted to: {buffer[offset]} {buffer[offset + 1]} => 0")
i += 1
except IndexError:
print('The code section in this executable is not enough to hide this message.')
print(f'Still {len(binary_data) - i} bits left to hide.')
print('Program exits')
clear_logs(exe_name)
exit(1)
return buffer
def write_buffer(buffer, executable, path):
"""Write buffer back to hard-disk (physical memory) in current directory"""
if path:
if path[-1] == '/':
path = f"{path[0:-1]}/{executable}"
else:
path = f"{executable}"
with open(path, "wb") as file:
file.write(buffer)
def modify_buffer(buffer, binary_data, offsets_list, exe_name=None):
"""Modify buffer (executable file) according to binary input (file).
:param buffer: executable file to modify.
:param binary_data: binary string to hide inside executable file.
:param offsets_list: list containing tuples of offsets and opcodes."""
# Getting only offsets in list of tuples
offsets = [int(x[0]) for x in offsets_list]
# Calculate starting offset to start decoding actual data
# The first 32-bits are saved to mark the start and end of data
# start_binary = '0' * (16 - len(bin(offsets[32])[2:])) + bin(offsets[32])[2:]
try:
end_binary = '0' * (16 - len(bin(offsets[len(binary_data) + 15])[2:])) + bin(offsets[len(binary_data) + 15])[2:]
except IndexError:
print(f"Not enough offsets to decode bits with, need executable with bigger code section")
print(f"Program exits")
clear_logs(exe_name)
exit(1)
# print(f"Start offset: {int(start_binary,2)}")
# print(f"End offset: {int(end_binary, 2)}")
# Concatenate binary end mark with binary data
full_binary_data = end_binary + binary_data
# Decode 'full_binary_data' within executable file
decode_data_within_executable(buffer, full_binary_data, offsets, exe_name)
return buffer
def clear_logs(exe_name):
exe_name = os.path.basename(exe_name).split('.')[0]
os.system(f'del {exe_name}_code_offset.txt, {exe_name}_dump.txt')
if __name__ == '__main__':
# Get arguments from CLI
args = get_arguments_cli()
# Path to file for hiding or extracting
data = args.data
# Path to executable file
executable = args.executable
# Path of modified executable
output_path = args.output_path
# Get targeted mnemonics offsets from executable's object data
offsets_list = get_executable_offsets(executable)
# Get file's binary data
binary_data = get_file_binary_data(data)
# Load executable's data into buffer
buffer = get_executable_binary(executable)
# Get executable's name from path
exe_name = os.path.basename(executable)
# Modify buffer according to 'binary_data'
buffer = modify_buffer(buffer, binary_data, offsets_list, exe_name)
# Write modified buffer back to hard-disk (looks exactly like original)
write_buffer(buffer, exe_name, output_path)
# Delete executable's object data logs
clear_logs(exe_name)