Skip to content

Commit

Permalink
Merge pull request #500 from PensiveHike/main
Browse files Browse the repository at this point in the history
Added Samsung My Files Operation History and Trash decoders
  • Loading branch information
abrignoni authored Jun 18, 2024
2 parents e078c33 + 611a43f commit b60b902
Show file tree
Hide file tree
Showing 2 changed files with 318 additions and 0 deletions.
130 changes: 130 additions & 0 deletions scripts/artifacts/smyfilesOpHistory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
from scripts.artifact_report import ArtifactHtmlReport
from scripts.ilapfuncs import logfunc, tsv, timeline, is_platform_windows, open_sqlite_db_readonly
import scripts.artifacts.artGlobals as aG
from packaging import version

__artifacts_v2__ = {
"smyfilesOpHistory": {
"name": "My Files Operation History",
"description": "Extracts Operation History from My Files database",
"author": "@PensiveHike",
"version": "0.1",
"date": "2024-06-05",
"requirements": "none",
"category": "My Files",
"notes": "Current decode works with Android versions 10-12. Subscripted/accented/unknown characters will be replaced with '?'.",
"paths": '*/com.sec.android.app.myfiles/databases/OperationHistory.db*',
"function": "get_smyfiles_OpHistory"
}
}

# database locations found at
# /data/data/com.sec.android.app.myfiles/databases/OperationHistory.db
# /data/user/150/com.sec.android.app.myfiles/databases/OperationHistory.db


def check_value(value):
decode_dic = {'a': 'b', 'b': 'a', 'c': 'c', 'd': 'd', 'e': 'f', 'f': 'e', 'g': 'g', 'h': 'p', 'i': 'r', 'j': 'q',
'k': 's', 'l': 't', 'm': 'v', 'n': 'u', 'o': 'w', 'p': 'h', 'q': 'j', 'r': 'i', 's': 'k', 't': 'l',
'u': 'n', 'v': 'm', 'w': 'o', 'x': 'x', 'y': 'z', 'z': 'y', '0': '(', '1': '*', '2': ')', '3': '+',
'4': ',', '5': '.', '6': '-', '7': '/', '8': '8', '9': ':', '(': '0', '*': '1', ')': '2', '+': '3',
',': '4', '.': '5', '-': '6', '/': '7', ':': '9', ' ': ' ', '_': '_', '[': '[', ']': '^', '^': ']',
'£': '£', '&': '%', '%': '&', '\'': '\'', '$': '$', '\"': '!', '!': '\"', '#': '#', '@': '@'}
if value in decode_dic:
result = decode_dic[value]
else:
result = '?'
return result


def process_string(old_string):
new_string = ''
for char in old_string:

if char.isalpha() and char.isupper():
char = check_value(char.lower())
char = char.upper()
else:
char = check_value(char)
new_string += char
return new_string


def get_db_data(file_found):
db = open_sqlite_db_readonly(file_found)
cursor = db.cursor()

try:
cursor.execute("""Select * from operation_history""")
all_rows = cursor.fetchall()
except:
all_rows = 0

return all_rows


def get_user(file_found):
"""If record located within secure folder path, retrieve user value (usually 150)"""
if '/user/1' in file_found:
finder = file_found.find('/user/')
start = finder + 6
end = start + 3
user = file_found[start:end]
else:
user = 0
return user


def get_smyfiles_OpHistory(files_found, report_folder, seeker, wrap_text, time_offset):
html_source = ''
Androidversion = aG.versionf

if not 9 < int(Androidversion) < 13:
logfunc(f'Android artifact My Files Operation History is not compatible with Android version {Androidversion}')

else:
data_list = []
for file_found in files_found:
file_found = str(file_found)
user = get_user(file_found)
if file_found.lower().endswith('.db'):
html_source = file_found
all_rows = get_db_data(file_found)
usageentries = 0
if all_rows != 0:
usageentries = len(all_rows)

if usageentries > 0:
for entry in all_rows:
cipher = entry[1]
# mod_ts = entry[2]
# op_type = entry[3]
# item_count = entry[4]
# folder_count = entry[5]
# page_type = entry[6]
#print(repr(cipher))
if cipher != '':
clear_path = process_string(cipher)
elif cipher[:3] == '#G$': # not supported, have seen in Android 13/14.
clear_path = '*unsupported entry*'
else:
clear_path = '*blank entry*'
data_list.append((user, clear_path, entry[2], entry[3], entry[4], entry[5], entry[6]))

if data_list:
report = ArtifactHtmlReport('My Files DB - Operation History')
report.start_artifact_report(report_folder, 'My Files DB - Operation History')
report.add_script()
data_headers = ('Account', 'Item Path', 'Operation Date (Handset Timezone)', 'Operation Type',
'Item Count', 'Folder Count', 'Page Type')
report.write_artifact_data_table(data_headers, data_list, html_source)
report.end_artifact_report()

tsvname = f'My Files db - Operation History'
tsv(report_folder, data_headers, data_list, tsvname)

tlactivity = f'My Files DB - Operation History'
timeline(report_folder, tlactivity, data_list, data_headers)
else:
logfunc('No My Files DB Operation History data available')

188 changes: 188 additions & 0 deletions scripts/artifacts/smyfilesTrash.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import shutil

from scripts.artifact_report import ArtifactHtmlReport
from scripts.ilapfuncs import logfunc, tsv, timeline, is_platform_windows, media_to_html
from scripts.filetype import guess_mime
import datetime
import os

__artifacts_v2__ = {
"smyfilesTrash": {
"name": "My Files Trash",
"description": "Shows Original Location and Deletion Timestamp of files/folders within My Files Trash",
"author": "@PensiveHike",
"version": "0.1",
"date": "2024-06-05",
"requirements": "none",
"category": "My Files",
"notes": "Timestamp corroborated with My Files Operation History database",
"paths": ('*/com.sec.android.app.myfiles/files/trash/*', '*/.Trash/com.sec.android.app.myfiles/*'),
"function": "get_smyfiles_trash"
}
}

# example filepaths
# /data/media/0/Android/data/com.sec.android.app.myfiles/files/trash/%.'ar74b%a$&7rcrGZ-Y.5$QT2/1639144040510/storage/emulated/0/Download/.!%#@$/Untitled.mov
# /data/media/0/Android/.Trash/com.sec.android.app.myfiles/09b6cc33-bd68-46ee-8fbd-a147b0348f1aT3/1714648136151/storage/emulated/0/DCIM/Camera/.!%#@$/20240502_120754.mp4

platform = is_platform_windows()
if platform:
splitter = '\\'
else:
splitter = '/'


def relative_paths(source):
splitted_a = source.split(splitter)
for x in splitted_a:
if 'LEAPP_Reports_' in x:
report_folder = x

splitted_b = source.split(report_folder)
return '.' + splitted_b[1]


def modded_media_to_html(input):
source = relative_paths(input)
mimetype = guess_mime(input)
if mimetype == None:
mimetype = ''
filename = os.path.basename(input)
if 'video' in mimetype:
thumb = f'<video width="320" height="240" controls="controls"><source src="{source}" type="video/mp4" preload="none">Your browser does not support the video tag.</video>'
elif 'image' in mimetype:
thumb = f'<a href="{source}" target="_blank"><img src="{source}"width="300"></img></a>'
elif 'audio' in mimetype:
thumb = f'<audio controls><source src="{source}" type="audio/ogg"><source src="{source}" type="audio/mpeg">Your browser does not support the audio element.</audio>'
else:
thumb = f'<a href="{source}" target="_blank"> Link to {filename} file</>'
return thumb


def get_smyfiles_trash(files_found, report_folder, seeker, wrap_text, time_offset):
if files_found:
separator = ".!%#@$"
data_list = []
html_source = ''
# Keep Trash files together as opposed to filling the My Files report folder
report_folder = os.path.join(report_folder, 'Trash')
if not os.path.exists(report_folder):
os.mkdir(report_folder)

for file_found in files_found:
# only want to process files or (empty) folders at the end of the chain, not folders mid-chain
if (separator in file_found
and not file_found.endswith(separator)
and (os.path.isfile(file_found) or (os.path.isdir(file_found) and not os.listdir(file_found)))):
parts = file_found.split('\\')
path_length = len(parts)

app_loc = parts.index("com.sec.android.app.myfiles")

# GET USER FROM PATH
user = parts[(app_loc-3)]
try:
if user.isdigit():
pass
except ValueError:
user = ''

# version affects location of folders within filepath and their relative index
if '.Trash' in parts:
folder_index = 0
else:
folder_index = 2

# GET TIMESTAMP FROM PATH (FROM TESTING THIS IS UTC)
timestamp_position = app_loc + folder_index + 2
timestamp = parts[timestamp_position]
# catch timestamp error in case index count is wrong
try:
converted_timestamp = datetime.datetime.utcfromtimestamp(int(timestamp) / 1000).strftime('%Y-%m-%d %H:%M:%S')
except ValueError:
converted_timestamp = ''

# PROVIDE SOURCE TO RESULTING HTML REPORT
if html_source == '':
html_source = '\\'.join(parts[:timestamp_position])

# GET ORIGINAL FILE/FOLDER LOCATION FROM PATH
separator_loc = parts.index(separator)
orig_loc_start_pos = timestamp_position + 1
original_location = '\\'

for i in range(orig_loc_start_pos, separator_loc, 1):
original_location += parts[i] + '\\'

# GET FILES/FOLDERS MARKED FOR DELETION
num_items = path_length - separator_loc - 1
if num_items == 1: # a file
marked_for_deletion = parts[-1]
marked_for_deletion_contents = ''
else: # a folder
marked_for_deletion = '\\' + parts[separator_loc + 1] + '\\'

marked_for_deletion_contents = '\\'
for i in range(separator_loc + 1, path_length, 1):
#print(parts[i])
marked_for_deletion_contents += parts[i] + '\\'
# remove final backslash
marked_for_deletion_contents = marked_for_deletion_contents[:-1]

# GET RECORD FILEPATH FOR REPORT
start = app_loc - 5
record_filepath = '\\'
for i in range(start, path_length, 1):
record_filepath += parts[i] + '\\'
# remove final backslash
record_filepath = record_filepath[:-1]

# PROVIDE LINK TO MEDIA IN REPORT
# If the file's parent is the separator, media does not play in report.
# The limitation of media to html, is it places the media file within a folder titled by its parent
# If the file Camera\ABC.jpg is deleted multiple times and has the same filename for multiple images,
# the same image would display for each record. Therefore, have output media to unique locations based
# on unix parent.
# Using media_to_html does not keep this uniqueness, so have copied sections from that function
# to use within this script
record_for_log = ''
if os.path.isfile(file_found) and os.path.getsize(file_found) > 0:
media_file_path = report_folder

# add folders to path and create a folder at each stage if required
for entry in parts[timestamp_position:-1]:
if entry != separator:
media_file_path = os.path.join(media_file_path, entry)
record_for_log = os.path.join(record_for_log, entry)
if not os.path.exists(media_file_path):
os.mkdir(media_file_path)
record_for_log = os.path.join(record_for_log, parts[-1])
logfunc(f"Processing {record_for_log}")

media_file_path = os.path.join(media_file_path, parts[-1])
shutil.copy2(file_found, media_file_path)

thumb = modded_media_to_html(media_file_path)
else:
thumb = ''

data_list.append((thumb, record_filepath, user, converted_timestamp, original_location, marked_for_deletion, marked_for_deletion_contents))

if data_list:
report = ArtifactHtmlReport('My Files - Trash Folder')
report.start_artifact_report(report_folder, 'My Files - Trash Folder')
report.add_script()
data_headers = ('Media', 'Record Path', 'Account', 'Marked for Deletion Timestamp', 'Original Location',
'File/Folder Marked For Deletion', 'Contents of Folder Marked for Deletion')
report.write_artifact_data_table(data_headers, data_list, html_source, html_no_escape=['Media'])
report.end_artifact_report()

tsvname = f'My Files - Trash Folder'
tsv(report_folder, data_headers, data_list, tsvname)

tlactivity = f'My Files - Trash Folder'
timeline(report_folder, tlactivity, data_list, data_headers)

else:
logfunc('Nothing Located within My Files Trash')

0 comments on commit b60b902

Please sign in to comment.