diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/AddLogicalImageTask.java b/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/AddLogicalImageTask.java index be85936754..f5d728b633 100644 --- a/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/AddLogicalImageTask.java +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/AddLogicalImageTask.java @@ -64,6 +64,8 @@ final class AddLogicalImageTask implements Runnable { private final static String MODULE_NAME = "Logical Imager"; //NON-NLS private final static String ROOT_STR = "root"; // NON-NLS private final static String VHD_EXTENSION = ".vhd"; // NON-NLS + private final static int REPORT_PROGRESS_INTERVAL = 100; + private final static int POST_ARTIFACT_INTERVAL = 1000; private final String deviceId; private final String timeZone; private final File src; @@ -145,7 +147,7 @@ final class AddLogicalImageTask implements Runnable { callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS, errorList, emptyDataSources); return; } - + // Add the SearchResults.txt and users.txt to the case report String resultsFilename; if (Paths.get(dest.toString(), SEARCH_RESULTS_TXT).toFile().exists()) { @@ -200,7 +202,7 @@ final class AddLogicalImageTask implements Runnable { List newDataSources = new ArrayList<>(); Map> interestingFileMap = new HashMap<>(); - + if (imagePaths.isEmpty()) { createVHD = false; // No VHD in src directory, try ingest the root directory as local files @@ -241,7 +243,7 @@ final class AddLogicalImageTask implements Runnable { if (addMultipleImagesTask.getResult() == DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS) { LOGGER.log(Level.SEVERE, "Failed to add VHD datasource"); // NON-NLS callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS, addMultipleImagesTask.getErrorMessages(), emptyDataSources); - return; + return; } try { interestingFileMap = getInterestingFileMapForVHD(Paths.get(dest.toString(), resultsFilename)); @@ -250,7 +252,7 @@ final class AddLogicalImageTask implements Runnable { LOGGER.log(Level.SEVERE, "Failed to add interesting files", ex); // NON-NLS callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.NONCRITICAL_ERRORS, errorList, emptyDataSources); } - + } catch (NoCurrentCaseException ex) { String msg = Bundle.AddLogicalImageTask_noCurrentCase(); errorList.add(msg); @@ -261,7 +263,7 @@ final class AddLogicalImageTask implements Runnable { if (cancelled) { if (!createVHD) { - // TODO: When 5453 is fixed, we should be able to delete it when adding VHD. + // TODO: When 5453 is fixed, we should be able to delete it when adding VHD. deleteDestinationDirectory(); } errorList.add(Bundle.AddLogicalImageTask_addImageCancelled()); @@ -345,7 +347,7 @@ final class AddLogicalImageTask implements Runnable { private void addInterestingFiles(Map> interestingFileMap) throws IOException, TskCoreException { int lineNumber = 0; List artifacts = new ArrayList<>(); - + Iterator>> iterator = interestingFileMap.entrySet().iterator(); while (iterator.hasNext()) { @@ -365,21 +367,23 @@ final class AddLogicalImageTask implements Runnable { List fileIds = entry.getValue(); for (Long fileId: fileIds) { - if (lineNumber % 100 == 0) { + if (cancelled) { + postArtifacts(artifacts); + return; + } + if (lineNumber % REPORT_PROGRESS_INTERVAL == 0) { progressMonitor.setProgressText(Bundle.AddLogicalImageTask_addingInterestingFile(lineNumber, totalFiles)); } + if (lineNumber % POST_ARTIFACT_INTERVAL == 0) { + postArtifacts(artifacts); + artifacts.clear(); + } addInterestingFileToArtifacts(fileId, ruleSetName, ruleName, artifacts); lineNumber++; } iterator.remove(); } - - try { - // index the artifact for keyword search - blackboard.postArtifacts(artifacts, MODULE_NAME); - } catch (Blackboard.BlackboardException ex) { - LOGGER.log(Level.SEVERE, "Unable to post artifacts to blackboard", ex); //NON-NLS - } + postArtifacts(artifacts); } private void addInterestingFileToArtifacts(long fileId, String ruleSetName, String ruleName, List artifacts) throws TskCoreException { @@ -400,7 +404,7 @@ final class AddLogicalImageTask implements Runnable { Map> objIdToimagePathsMap = currentCase.getSleuthkitCase().getImagePaths(); imagePathToObjIdMap = imagePathsToDataSourceObjId(objIdToimagePathsMap); Map> interestingFileMap = new HashMap<>(); - + try (BufferedReader br = new BufferedReader(new InputStreamReader( new FileInputStream(resultsPath.toFile()), "UTF8"))) { // NON-NLS String line; @@ -426,7 +430,7 @@ final class AddLogicalImageTask implements Runnable { String filename = fields[7]; String parentPath = fields[8]; - if (lineNumber % 100 == 0) { + if (lineNumber % REPORT_PROGRESS_INTERVAL == 0) { progressMonitor.setProgressText(Bundle.AddLogicalImageTask_searchingInterestingFile(lineNumber, totalFiles)); } @@ -443,11 +447,20 @@ final class AddLogicalImageTask implements Runnable { interestingFileMap.put(key, fileIds); } lineNumber++; - } // end reading file } + } // end reading file } return interestingFileMap; } + private void postArtifacts(List artifacts) { + try { + // index the artifact for keyword search + blackboard.postArtifacts(artifacts, MODULE_NAME); + } catch (Blackboard.BlackboardException ex) { + LOGGER.log(Level.SEVERE, "Unable to post artifacts to blackboard", ex); //NON-NLS + } + } + @Messages({ "# {0} - file number", "# {1} - total files", "AddLogicalImageTask.addingExtractedFile=Adding extracted files ({0}/{1})" }) @@ -455,7 +468,7 @@ final class AddLogicalImageTask implements Runnable { SleuthkitCase skCase = Case.getCurrentCase().getSleuthkitCase(); SleuthkitCase.CaseDbTransaction trans = null; Map> interestingFileMap = new HashMap<>(); - + try { trans = skCase.beginTransaction(); LocalFilesDataSource localFilesDataSource = skCase.addLocalFilesDataSource(deviceId, this.src.getName(), timeZone, trans); @@ -492,7 +505,7 @@ final class AddLogicalImageTask implements Runnable { String ctime = fields[13]; parentPath = ROOT_STR + "/" + vhdFilename + "/" + parentPath; - if (lineNumber % 100 == 0) { + if (lineNumber % REPORT_PROGRESS_INTERVAL == 0) { progressMonitor.setProgressText(Bundle.AddLogicalImageTask_addingExtractedFile(lineNumber, totalFiles)); } diff --git a/InternalPythonModules/android/ResultSetIterator.py b/InternalPythonModules/android/ResultSetIterator.py new file mode 100644 index 0000000000..4abd4438df --- /dev/null +++ b/InternalPythonModules/android/ResultSetIterator.py @@ -0,0 +1,35 @@ +""" +Autopsy Forensic Browser + +Copyright 2019 Basis Technology Corp. +Contact: carrier sleuthkit org + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +class ResultSetIterator(object): + """ + Generic base class for iterating through database recordms + """ + + def __init__(self, result_set): + self.result_set = result_set + + def next(self): + if self.result_set is None: + return False + return self.result_set.next() + + def close(self): + if self.result_set is not None: + self.result_set.close() diff --git a/InternalPythonModules/android/TskCallLogsParser.py b/InternalPythonModules/android/TskCallLogsParser.py new file mode 100644 index 0000000000..d4e6942134 --- /dev/null +++ b/InternalPythonModules/android/TskCallLogsParser.py @@ -0,0 +1,65 @@ +""" +Autopsy Forensic Browser + +Copyright 2019 Basis Technology Corp. +Contact: carrier sleuthkit org + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" +from ResultSetIterator import ResultSetIterator +from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import CallMediaType +from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import CommunicationDirection +from org.sleuthkit.datamodel import Account + +class TskCallLogsParser(ResultSetIterator): + """ + Generic TSK_CALLLOG artifact template. Each of these methods + will contain the extraction and transformation logic for + converting raw database records to the expected TSK_CALLLOG + format. + + A simple example of data transformation would be computing + the end time of a call when the database only supplies the start + time and duration. + """ + + def __init__(self, result_set): + super(TskCallLogsParser, self).__init__(result_set) + self._DEFAULT_STRING = "" + self._DEFAULT_DIRECTION = CommunicationDirection.UNKNOWN + self._DEFAULT_ADDRESS = None + self._DEFAULT_CALL_TYPE = CallMediaType.UNKNOWN + self._DEFAULT_LONG = -1L + + self.INCOMING_CALL = CommunicationDirection.INCOMING + self.OUTGOING_CALL = CommunicationDirection.OUTGOING + self.AUDIO_CALL = CallMediaType.AUDIO + self.VIDEO_CALL = CallMediaType.VIDEO + + def get_call_direction(self): + return self._DEFAULT_DIRECTION + + def get_phone_number_from(self): + return self._DEFAULT_ADDRESS + + def get_phone_number_to(self): + return self._DEFAULT_ADDRESS + + def get_call_start_date_time(self): + return self._DEFAULT_LONG + + def get_call_end_date_time(self): + return self._DEFAULT_LONG + + def get_call_type(self): + return self._DEFAULT_CALL_TYPE diff --git a/InternalPythonModules/android/TskContactsParser.py b/InternalPythonModules/android/TskContactsParser.py new file mode 100644 index 0000000000..122e6a9445 --- /dev/null +++ b/InternalPythonModules/android/TskContactsParser.py @@ -0,0 +1,49 @@ +""" +Autopsy Forensic Browser + +Copyright 2019 Basis Technology Corp. +Contact: carrier sleuthkit org + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" +from ResultSetIterator import ResultSetIterator + +class TskContactsParser(ResultSetIterator): + """ + Generic TSK_CONTACT artifact template. Each of these methods + will contain the extraction and transformation logic for + converting raw database records to the expected TSK_CONTACT + format. + """ + + def __init__(self, result_set): + super(TskContactsParser, self).__init__(result_set) + self._DEFAULT_VALUE = "" + + def get_account_name(self): + return self._DEFAULT_VALUE + + def get_contact_name(self): + return self._DEFAULT_VALUE + + def get_phone(self): + return self._DEFAULT_VALUE + + def get_home_phone(self): + return self._DEFAULT_VALUE + + def get_mobile_phone(self): + return self._DEFAULT_VALUE + + def get_email(self): + return self._DEFAULT_VALUE diff --git a/InternalPythonModules/android/TskMessagesParser.py b/InternalPythonModules/android/TskMessagesParser.py new file mode 100644 index 0000000000..4568a7400c --- /dev/null +++ b/InternalPythonModules/android/TskMessagesParser.py @@ -0,0 +1,73 @@ +""" +Autopsy Forensic Browser + +Copyright 2019 Basis Technology Corp. +Contact: carrier sleuthkit org + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" +from ResultSetIterator import ResultSetIterator +from org.sleuthkit.datamodel import Account +from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import MessageReadStatus +from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import CommunicationDirection + +class TskMessagesParser(ResultSetIterator): + """ + Generic TSK_MESSAGE artifact template. Each of these methods + will contain the extraction and transformation logic for + converting raw database records to the expected TSK_MESSAGE + format. + + An easy example of such a transformation would be converting + message date time from milliseconds to seconds. + """ + + def __init__(self, result_set): + super(TskMessagesParser, self).__init__(result_set) + self._DEFAULT_TEXT = "" + self._DEFAULT_LONG = -1L + self._DEFAULT_MSG_READ_STATUS = MessageReadStatus.UNKNOWN + self._DEFAULT_ACCOUNT_ADDRESS = None + self._DEFAULT_COMMUNICATION_DIRECTION = CommunicationDirection.UNKNOWN + + self.INCOMING = CommunicationDirection.INCOMING + self.OUTGOING = CommunicationDirection.OUTGOING + self.READ = MessageReadStatus.READ + self.UNREAD = MessageReadStatus.UNREAD + + def get_message_type(self): + return self._DEFAULT_TEXT + + def get_message_direction(self): + return self._DEFAULT_COMMUNICATION_DIRECTION + + def get_phone_number_from(self): + return self._DEFAULT_ACCOUNT_ADDRESS + + def get_phone_number_to(self): + return self._DEFAULT_ACCOUNT_ADDRESS + + def get_message_date_time(self): + return self._DEFAULT_LONG + + def get_message_read_status(self): + return self._DEFAULT_MSG_READ_STATUS + + def get_message_subject(self): + return self._DEFAULT_TEXT + + def get_message_text(self): + return self._DEFAULT_TEXT + + def get_thread_id(self): + return self._DEFAULT_TEXT diff --git a/InternalPythonModules/android/general.py b/InternalPythonModules/android/general.py index 1d0832362b..53c123d13c 100644 --- a/InternalPythonModules/android/general.py +++ b/InternalPythonModules/android/general.py @@ -27,8 +27,6 @@ class AndroidComponentAnalyzer: def analyze(self, dataSource, fileManager, context): raise NotImplementedError - - """ A utility method to append list of attachments to msg body """ @@ -39,5 +37,3 @@ def appendAttachmentList(msgBody, attachmentsList): body = body + "\n".join(attachmentsList) return body - - diff --git a/InternalPythonModules/android/imo.py b/InternalPythonModules/android/imo.py index aed3b4011c..714c029445 100644 --- a/InternalPythonModules/android/imo.py +++ b/InternalPythonModules/android/imo.py @@ -29,6 +29,7 @@ from java.util.logging import Level from java.util import ArrayList from org.apache.commons.codec.binary import Base64 from org.sleuthkit.autopsy.casemodule import Case +from org.sleuthkit.autopsy.casemodule import NoCurrentCaseException from org.sleuthkit.autopsy.coreutils import Logger from org.sleuthkit.autopsy.coreutils import MessageNotifyUtil from org.sleuthkit.autopsy.coreutils import AppSQLiteDB @@ -56,10 +57,14 @@ and adds artifacts to the case. class IMOAnalyzer(general.AndroidComponentAnalyzer): def __init__(self): self._logger = Logger.getLogger(self.__class__.__name__) + self._PACKAGE_NAME = "com.imo.android.imous" + self._PARSER_NAME = "IMO Parser" + self._MESSAGE_TYPE = "IMO Message" + self._VERSION = "9.8.0" def analyze(self, dataSource, fileManager, context): selfAccountAddress = None - accountDbs = AppSQLiteDB.findAppDatabases(dataSource, "accountdb.db", True, "com.imo.android.imous") + accountDbs = AppSQLiteDB.findAppDatabases(dataSource, "accountdb.db", True, self._PACKAGE_NAME) for accountDb in accountDbs: try: accountResultSet = accountDb.runQuery("SELECT uid, name FROM account") @@ -71,16 +76,26 @@ class IMOAnalyzer(general.AndroidComponentAnalyzer): selfAccountAddress = Account.Address(accountResultSet.getString("uid"), accountResultSet.getString("name")) except SQLException as ex: - self._logger.log(Level.SEVERE, "Error processing query result for account", ex) + self._logger.log(Level.WARNING, "Error processing query result for account", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) finally: accountDb.close() - friendsDbs = AppSQLiteDB.findAppDatabases(dataSource, "imofriends.db", True, "com.imo.android.imous") + friendsDbs = AppSQLiteDB.findAppDatabases(dataSource, "imofriends.db", True, self._PACKAGE_NAME) for friendsDb in friendsDbs: try: - friendsDBHelper = CommunicationArtifactsHelper(Case.getCurrentCase().getSleuthkitCase(), - "IMO Parser", friendsDb.getDBFile(), + current_case = Case.getCurrentCaseThrows() + if selfAccountAddress is not None: + friendsDBHelper = CommunicationArtifactsHelper(current_case.getSleuthkitCase(), + self._PARSER_NAME, + friendsDb.getDBFile(), Account.Type.IMO, Account.Type.IMO, selfAccountAddress ) + else: + friendsDBHelper = CommunicationArtifactsHelper(current_case.getSleuthkitCase(), + self._PARSER_NAME, + friendsDb.getDBFile(), + Account.Type.IMO + ) contactsResultSet = friendsDb.runQuery("SELECT buid, name FROM friends") if contactsResultSet is not None: while contactsResultSet.next(): @@ -121,7 +136,7 @@ class IMOAnalyzer(general.AndroidComponentAnalyzer): messageArtifact = friendsDBHelper.addMessage( - "IMO Message", + self._MESSAGE_TYPE, direction, fromAddress, toAddress, @@ -137,8 +152,16 @@ class IMOAnalyzer(general.AndroidComponentAnalyzer): except SQLException as ex: self._logger.log(Level.WARNING, "Error processing query result for IMO friends", ex) - except (TskCoreException, BlackboardException) as ex: - self._logger.log(Level.WARNING, "Failed to create IMO message artifacts.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + except TskCoreException as ex: + self._logger.log(Level.SEVERE, "Failed to add IMO message artifacts.", ex) + self._logger.log(Level.SEVERE, traceback.format_exc()) + except BlackboardException as ex: + self._logger.log(Level.WARNING, "Failed to post artifacts.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + except NoCurrentCaseException as ex: + self._logger.log(Level.WARNING, "No case currently open.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) finally: friendsDb.close() diff --git a/InternalPythonModules/android/line.py b/InternalPythonModules/android/line.py new file mode 100644 index 0000000000..c87ef3477c --- /dev/null +++ b/InternalPythonModules/android/line.py @@ -0,0 +1,386 @@ +""" +Autopsy Forensic Browser + +Copyright 2019 Basis Technology Corp. +Contact: carrier sleuthkit org + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" +from java.io import File +from java.lang import Class +from java.lang import ClassNotFoundException +from java.lang import Long +from java.lang import String +from java.sql import ResultSet +from java.sql import SQLException +from java.sql import Statement +from java.util.logging import Level +from java.util import ArrayList +from org.apache.commons.codec.binary import Base64 +from org.sleuthkit.autopsy.casemodule import Case +from org.sleuthkit.autopsy.coreutils import Logger +from org.sleuthkit.autopsy.coreutils import MessageNotifyUtil +from org.sleuthkit.autopsy.coreutils import AppSQLiteDB + +from org.sleuthkit.autopsy.datamodel import ContentUtils +from org.sleuthkit.autopsy.ingest import IngestJobContext +from org.sleuthkit.datamodel import AbstractFile +from org.sleuthkit.datamodel import BlackboardArtifact +from org.sleuthkit.datamodel import BlackboardAttribute +from org.sleuthkit.datamodel import Content +from org.sleuthkit.datamodel import TskCoreException +from org.sleuthkit.datamodel.Blackboard import BlackboardException +from org.sleuthkit.autopsy.casemodule import NoCurrentCaseException +from org.sleuthkit.datamodel import Account +from org.sleuthkit.datamodel.blackboardutils import CommunicationArtifactsHelper +from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import MessageReadStatus +from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import CommunicationDirection +from TskContactsParser import TskContactsParser +from TskMessagesParser import TskMessagesParser +from TskCallLogsParser import TskCallLogsParser + +import traceback +import general + +class LineAnalyzer(general.AndroidComponentAnalyzer): + """ + Parses the Line App databases for TSK contacts & message artifacts. + """ + + def __init__(self): + self._logger = Logger.getLogger(self.__class__.__name__) + self._LINE_PACKAGE_NAME = "jp.naver.line.android" + self._PARSER_NAME = "Line Parser" + self._VERSION = "9.15.1" + + def analyze(self, dataSource, fileManager, context): + try: + contact_and_message_dbs = AppSQLiteDB.findAppDatabases(dataSource, + "naver_line", True, self._LINE_PACKAGE_NAME) + calllog_dbs = AppSQLiteDB.findAppDatabases(dataSource, + "call_history", True, self._LINE_PACKAGE_NAME) + + for contact_and_message_db in contact_and_message_dbs: + current_case = Case.getCurrentCaseThrows() + helper = CommunicationArtifactsHelper( + current_case.getSleuthkitCase(), self._PARSER_NAME, + contact_and_message_db.getDBFile(), Account.Type.LINE) + self.parse_contacts(contact_and_message_db, helper) + self.parse_messages(contact_and_message_db, helper) + + for calllog_db in calllog_dbs: + current_case = Case.getCurrentCaseThrows() + helper = CommunicationArtifactsHelper( + current_case.getSleuthkitCase(), self._PARSER_NAME, + calllog_db.getDBFile(), Account.Type.LINE) + self.parse_calllogs(dataSource, calllog_db, helper) + + except NoCurrentCaseException as ex: + # Error parsing Line databases. + self._logger.log(Level.WARNING, "Error parsing the Line App Databases", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + + for contact_and_message_db in contact_and_message_dbs: + contact_and_message_db.close() + + for calllog_db in calllog_dbs: + calllog_db.close() + + def parse_contacts(self, contacts_db, helper): + try: + contacts_parser = LineContactsParser(contacts_db) + while contacts_parser.next(): + helper.addContact( + contacts_parser.get_account_name(), + contacts_parser.get_contact_name(), + contacts_parser.get_phone(), + contacts_parser.get_home_phone(), + contacts_parser.get_mobile_phone(), + contacts_parser.get_email() + ) + contacts_parser.close() + except SQLException as ex: + self._logger.log(Level.WARNING, "Error parsing the Line App Database for contacts", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + except TskCoreException as ex: + #Error adding artifact to case database... case is not complete. + self._logger.log(Level.SEVERE, + "Error adding Line contact artifacts to the case database.", ex) + self._logger.log(Level.SEVERE, traceback.format_exc()) + except BlackboardException as ex: + #Error posting notification to blackboard + self._logger.log(Level.WARNING, + "Error posting Line contact artifacts to blackboard.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + + def parse_calllogs(self, dataSource, calllogs_db, helper): + try: + calllogs_db.attachDatabase( + dataSource, "naver_line", + calllogs_db.getDBFile().getParentPath(), "naver") + + calllog_parser = LineCallLogsParser(calllogs_db) + while calllog_parser.next(): + helper.addCalllog( + calllog_parser.get_call_direction(), + calllog_parser.get_phone_number_from(), + calllog_parser.get_phone_number_to(), + calllog_parser.get_call_start_date_time(), + calllog_parser.get_call_end_date_time(), + calllog_parser.get_call_type() + ) + calllog_parser.close() + except SQLException as ex: + self._logger.log(Level.WARNING, "Error parsing the Line App Database for calllogs", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + except TskCoreException as ex: + #Error adding artifact to case database... case is not complete. + self._logger.log(Level.SEVERE, + "Error adding Line calllog artifacts to the case database.", ex) + self._logger.log(Level.SEVERE, traceback.format_exc()) + except BlackboardException as ex: + #Error posting notification to blackboard + self._logger.log(Level.WARNING, + "Error posting Line calllog artifacts to blackboard.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + + def parse_messages(self, messages_db, helper): + try: + + messages_parser = LineMessagesParser(messages_db) + while messages_parser.next(): + helper.addMessage( + messages_parser.get_message_type(), + messages_parser.get_message_direction(), + messages_parser.get_phone_number_from(), + messages_parser.get_phone_number_to(), + messages_parser.get_message_date_time(), + messages_parser.get_message_read_status(), + messages_parser.get_message_subject(), + messages_parser.get_message_text(), + messages_parser.get_thread_id() + ) + messages_parser.close() + except SQLException as ex: + self._logger.log(Level.WARNING, "Error parsing the Line App Database for messages.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + except TskCoreException as ex: + #Error adding artifact to case database... case is not complete. + self._logger.log(Level.SEVERE, + "Error adding Line message artifacts to the case database.", ex) + self._logger.log(Level.SEVERE, traceback.format_exc()) + except BlackboardException as ex: + #Error posting notification to blackboard + self._logger.log(Level.WARNING, + "Error posting Line message artifacts to blackboard.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + +class LineCallLogsParser(TskCallLogsParser): + """ + Parses out TSK_CALLLOG information from the Line database. + TSK_CALLLOG fields that are not in the line database are given + a default value inherited from the super class. + """ + + def __init__(self, calllog_db): + super(LineCallLogsParser, self).__init__(calllog_db.runQuery( + """ + SELECT substr(CallH.call_type, -1) AS direction, + CallH.start_time AS start_time, + CallH.end_time AS end_time, + ConT.server_name AS name, + CallH.voip_type AS call_type, + ConT.m_id + FROM call_history AS CallH + JOIN naver.contacts AS ConT + ON CallH.caller_mid = ConT.m_id + """ + ) + ) + self._OUTGOING_CALL_TYPE = "O" + self._INCOMING_CALL_TYPE = "I" + self._VIDEO_CALL_TYPE = "V" + self._AUDIO_CALL_TYPE = "A" + + def get_call_direction(self): + direction = self.result_set.getString("direction") + if direction == self._OUTGOING_CALL_TYPE: + return self.OUTGOING_CALL + return self.INCOMING_CALL + + def get_call_start_date_time(self): + try: + return long(self.result_set.getString("start_time")) / 1000 + except ValueError as ve: + return super(LineCallLogsParser, self).get_call_start_date_time() + + def get_call_end_date_time(self): + try: + return long(self.result_set.getString("end_time")) / 1000 + except ValueError as ve: + return super(LineCallLogsParser, self).get_call_end_date_time() + + def get_phone_number_to(self): + if self.get_call_direction() == self.OUTGOING_CALL: + return Account.Address(self.result_set.getString("m_id"), + self.result_set.getString("name")) + return super(LineCallLogsParser, self).get_phone_number_to() + + def get_phone_number_from(self): + if self.get_call_direction() == self.INCOMING_CALL: + return Account.Address(self.result_set.getString("m_id"), + self.result_set.getString("name")) + return super(LineCallLogsParser, self).get_phone_number_from() + + def get_call_type(self): + if self.result_set.getString("call_type") == self._VIDEO_CALL_TYPE: + return self.VIDEO_CALL + if self.result_set.getString("call_type") == self._AUDIO_CALL_TYPE: + return self.AUDIO_CALL + return super(LineCallLogsParser, self).get_call_type() + +class LineContactsParser(TskContactsParser): + """ + Parses out TSK_CONTACT information from the Line database. + TSK_CONTACT fields that are not in the line database are given + a default value inherited from the super class. + """ + + def __init__(self, contact_db): + super(LineContactsParser, self).__init__(contact_db.runQuery( + """ + SELECT m_id, + server_name + FROM contacts + """ + ) + ) + def get_account_name(self): + return self.result_set.getString("m_id") + + def get_contact_name(self): + return self.result_set.getString("server_name") + +class LineMessagesParser(TskMessagesParser): + """ + Parse out TSK_MESSAGE information from the Line database. + TSK_MESSAGE fields that are not in the line database are given + a default value inherited from the super class. + """ + + def __init__(self, message_db): + super(LineMessagesParser, self).__init__(message_db.runQuery( + """ + SELECT contact_list_with_groups.name, + contact_list_with_groups.id, + contact_list_with_groups.members, + contact_list_with_groups.member_names, + CH.from_mid, + C.server_name AS from_name, + CH.content, + CH.created_time, + CH.attachement_type, + CH.attachement_local_uri, + CH.status + FROM (SELECT G.name, + group_members.id, + group_members.members, + group_members.member_names + FROM (SELECT id, + group_concat(M.m_id) AS members, + group_concat(replace(C.server_name, + ",", + "")) as member_names + FROM membership AS M + JOIN contacts as C + ON M.m_id = C.m_id + GROUP BY id) AS group_members + JOIN groups AS G + ON G.id = group_members.id + UNION + SELECT server_name, + m_id, + NULL, + NULL + FROM contacts) AS contact_list_with_groups + JOIN chat_history AS CH + ON CH.chat_id = contact_list_with_groups.id + LEFT JOIN contacts as C + ON C.m_id = CH.from_mid + WHERE attachement_type != 6 + """ + ) + ) + self._LINE_MESSAGE_TYPE = "Line Message" + #From the limited test data, it appeared that incoming + #was only associated with a 1 status. Status # 3 and 7 + #was only associated with outgoing. + self._INCOMING_MESSAGE_TYPE = 1 + + def get_message_type(self): + return self._LINE_MESSAGE_TYPE + + def get_message_date_time(self): + created_time = self.result_set.getString("created_time") + try: + #Get time in seconds (created_time is stored in ms from epoch) + return long(created_time) / 1000 + except ValueError as ve: + return super(LineMessagesParser, self).get_message_date_time() + + def get_message_text(self): + content = self.result_set.getString("content") + attachment_uri = self.result_set.getString("attachement_local_uri") + if attachment_uri is not None and content is not None: + return general.appendAttachmentList(content, [attachment_uri]) + elif attachment_uri is not None and content is None: + return general.appendAttachmentList("", [attachment_uri]) + return content + + def get_message_direction(self): + if self.result_set.getInt("status") == self._INCOMING_MESSAGE_TYPE: + return self.INCOMING + return self.OUTGOING + + def get_phone_number_from(self): + if self.get_message_direction() == self.INCOMING: + from_mid = self.result_set.getString("from_mid") + if from_mid is not None: + return Account.Address(from_mid, + self.result_set.getString("from_name")) + return super(LineMessagesParser, self).get_phone_number_from() + + def get_phone_number_to(self): + if self.get_message_direction() == self.OUTGOING: + group = self.result_set.getString("members") + if group is not None: + group = group.split(",") + names = self.result_set.getString("member_names").split(",") + + recipients = [] + + for recipient_id, recipient_name in zip(group, names): + recipients.append(Account.Address(recipient_id, recipient_name)) + + return recipients + + return Account.Address(self.result_set.getString("id"), + self.result_set.getString("name")) + + return super(LineMessagesParser, self).get_phone_number_to() + + def get_thread_id(self): + members = self.result_set.getString("members") + if members is not None: + return self.result_set.getString("id") + return super(LineMessagesParser, self).get_thread_id() diff --git a/InternalPythonModules/android/module.py b/InternalPythonModules/android/module.py index 6ce91f419a..1e7e4b3844 100644 --- a/InternalPythonModules/android/module.py +++ b/InternalPythonModules/android/module.py @@ -50,6 +50,11 @@ import imo import xender import zapya import shareit +import viber +import skype +import line +import whatsapp +import textnow import sbrowser import operabrowser import oruxmaps @@ -101,6 +106,8 @@ class AndroidIngestModule(DataSourceIngestModule): googlemaplocation.GoogleMapLocationAnalyzer(), browserlocation.BrowserLocationAnalyzer(), cachelocation.CacheLocationAnalyzer(), imo.IMOAnalyzer(), xender.XenderAnalyzer(), zapya.ZapyaAnalyzer(), shareit.ShareItAnalyzer(), + line.LineAnalyzer(), whatsapp.WhatsAppAnalyzer(), + textnow.TextNowAnalyzer(), skype.SkypeAnalyzer(), viber.ViberAnalyzer(), sbrowser.SBrowserAnalyzer(), operabrowser.OperaAnalyzer(), oruxmaps.OruxMapsAnalyzer(), installedapps.InstalledApplicationsAnalyzer()] diff --git a/InternalPythonModules/android/shareit.py b/InternalPythonModules/android/shareit.py index bccc9b9a3b..937a663393 100644 --- a/InternalPythonModules/android/shareit.py +++ b/InternalPythonModules/android/shareit.py @@ -29,6 +29,7 @@ from java.util.logging import Level from java.util import ArrayList from org.apache.commons.codec.binary import Base64 from org.sleuthkit.autopsy.casemodule import Case +from org.sleuthkit.autopsy.casemodule import NoCurrentCaseException from org.sleuthkit.autopsy.coreutils import Logger from org.sleuthkit.autopsy.coreutils import MessageNotifyUtil from org.sleuthkit.autopsy.coreutils import AppSQLiteDB diff --git a/InternalPythonModules/android/skype.py b/InternalPythonModules/android/skype.py new file mode 100644 index 0000000000..f9bd6f5466 --- /dev/null +++ b/InternalPythonModules/android/skype.py @@ -0,0 +1,505 @@ +""" +Autopsy Forensic Browser + +Copyright 2019 Basis Technology Corp. +Contact: carrier sleuthkit org + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from java.io import File +from java.lang import Class +from java.lang import ClassNotFoundException +from java.lang import Long +from java.lang import String +from java.sql import ResultSet +from java.sql import SQLException +from java.sql import Statement +from java.util.logging import Level +from java.util import ArrayList +from org.apache.commons.codec.binary import Base64 +from org.sleuthkit.autopsy.casemodule import Case +from org.sleuthkit.autopsy.coreutils import Logger +from org.sleuthkit.autopsy.coreutils import MessageNotifyUtil +from org.sleuthkit.autopsy.coreutils import AppSQLiteDB + +from org.sleuthkit.autopsy.datamodel import ContentUtils +from org.sleuthkit.autopsy.ingest import IngestJobContext +from org.sleuthkit.datamodel import AbstractFile +from org.sleuthkit.datamodel import BlackboardArtifact +from org.sleuthkit.datamodel import BlackboardAttribute +from org.sleuthkit.datamodel import Content +from org.sleuthkit.datamodel import TskCoreException +from org.sleuthkit.datamodel.Blackboard import BlackboardException +from org.sleuthkit.autopsy.casemodule import NoCurrentCaseException +from org.sleuthkit.datamodel import Account +from org.sleuthkit.datamodel.blackboardutils import CommunicationArtifactsHelper +from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import MessageReadStatus +from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import CommunicationDirection +from TskMessagesParser import TskMessagesParser +from TskContactsParser import TskContactsParser +from TskCallLogsParser import TskCallLogsParser + +import traceback +import general + +class SkypeAnalyzer(general.AndroidComponentAnalyzer): + """ + Parses the Skype App databases for TSK contacts, message + and calllog artifacts. + + About version 8.15.0.428 (9/17/2019) Skype database: + - There are 4 tables this parser uses: + 1) person - this table appears to hold all contacts known to the user. + 2) user - this table holds information pertaining to the user. + 3) particiapnt - Yes, that is not a typo. This table maps group chat + ids to skype ids (1 to many). + 4) chatItem - This table contains all messages. It maps the group id or + skype id (for 1 to 1 communication) to the message content + and metadata. Either the group id or skype id is stored in + a column named 'conversation_link'. + + More info and implementation details: + - The person table does not include groups. To get + all 1 to 1 communications, we could simply join the person and chatItem tables. + This would mean we'd need to do a second pass to get all the group information + as they would be excluded in the join. Since the chatItem table stores both the + group id or skype_id in one column, an implementation decision was made to union + the person and particiapnt table together so that all rows are matched in one join + with chatItem. This result is consistently labeled contact_list_with_groups in the + following queries. + - In order to keep the formatting of the name consistent throughout each query, + a _format_user_name() function was created to encapsulate the CASE statement + that was being shared across them. Refer to the method for more details. + """ + + def __init__(self): + self._logger = Logger.getLogger(self.__class__.__name__) + self._SKYPE_PACKAGE_NAME = "com.skype.raider" + self._PARSER_NAME = "Skype Parser" + self._VERSION = "8.15.0.428" + + def get_user_account(self, skype_db): + account_query_result = skype_db.runQuery( + """ + SELECT entry_id, + """+_format_user_name()+""" AS name + FROM user + """ + ) + + if account_query_result is not None and account_query_result.next(): + return Account.Address(account_query_result.getString("entry_id"), + account_query_result.getString("name")) + return None + + def analyze(self, dataSource, fileManager, context): + #Skype databases are of the form: live:XYZ.db, where + #XYZ is the skype id of the user. The following search + #does a generic substring match for 'live' in the skype + #package. + skype_dbs = AppSQLiteDB.findAppDatabases(dataSource, + "live:", False, self._SKYPE_PACKAGE_NAME) + try: + for skype_db in skype_dbs: + #Attempt to get the user account id from the database + user_account_instance = None + try: + user_account_instance = self.get_user_account(skype_db) + except SQLException as ex: + self._logger.log(Level.WARNING, + "Error querying for the user account in the Skype db.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + + current_case = Case.getCurrentCaseThrows() + + if user_account_instance is None: + helper = CommunicationArtifactsHelper( + current_case.getSleuthkitCase(), self._PARSER_NAME, + skype_db.getDBFile(), Account.Type.SKYPE + ) + else: + helper = CommunicationArtifactsHelper( + current_case.getSleuthkitCase(), self._PARSER_NAME, + skype_db.getDBFile(), Account.Type.SKYPE, + Account.Type.SKYPE, user_account_instance + ) + self.parse_contacts(skype_db, helper) + self.parse_calllogs(skype_db, helper) + self.parse_messages(skype_db, helper) + except NoCurrentCaseException as ex: + self._logger.log(Level.WARNING, "No case currently open.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + + for skype_db in skype_dbs: + skype_db.close() + + def parse_contacts(self, skype_db, helper): + #Query for contacts and iterate row by row adding + #each contact artifact + try: + contacts_parser = SkypeContactsParser(skype_db) + while contacts_parser.next(): + helper.addContact( + contacts_parser.get_account_name(), + contacts_parser.get_contact_name(), + contacts_parser.get_phone(), + contacts_parser.get_home_phone(), + contacts_parser.get_mobile_phone(), + contacts_parser.get_email() + ) + contacts_parser.close() + except SQLException as ex: + #Error parsing Skype db + self._logger.log(Level.WARNING, + "Error parsing contact database for call logs artifacts.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + except TskCoreException as ex: + #Severe error trying to add to case database.. case is not complete. + #These exceptions are thrown by the CommunicationArtifactsHelper. + self._logger.log(Level.SEVERE, + "Failed to add contact artifacts to the case database.", ex) + self._logger.log(Level.SEVERE, traceback.format_exc()) + except BlackboardException as ex: + #Failed to post notification to blackboard + self._logger.log(Level.WARNING, + "Failed to post contact artifact to the blackboard", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + + def parse_calllogs(self, skype_db, helper): + #Query for call logs and iterate row by row adding + #each call log artifact + try: + calllog_parser = SkypeCallLogsParser(skype_db) + while calllog_parser.next(): + helper.addCalllog( + calllog_parser.get_call_direction(), + calllog_parser.get_phone_number_from(), + calllog_parser.get_phone_number_to(), + calllog_parser.get_call_start_date_time(), + calllog_parser.get_call_end_date_time(), + calllog_parser.get_call_type() + ) + calllog_parser.close() + except SQLException as ex: + #Error parsing Skype db + self._logger.log(Level.WARNING, + "Error parsing Skype database for call logs artifacts.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + except TskCoreException as ex: + #Severe error trying to add to case database.. case is not complete. + #These exceptions are thrown by the CommunicationArtifactsHelper. + self._logger.log(Level.SEVERE, + "Failed to add call log artifacts to the case database.", ex) + self._logger.log(Level.SEVERE, traceback.format_exc()) + except BlackboardException as ex: + #Failed to post notification to blackboard + self._logger.log(Level.WARNING, + "Failed to post call log artifact to the blackboard", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + + def parse_messages(self, skype_db, helper): + #Query for messages and iterate row by row adding + #each message artifact + try: + messages_parser = SkypeMessagesParser(skype_db) + while messages_parser.next(): + helper.addMessage( + messages_parser.get_message_type(), + messages_parser.get_message_direction(), + messages_parser.get_phone_number_from(), + messages_parser.get_phone_number_to(), + messages_parser.get_message_date_time(), + messages_parser.get_message_read_status(), + messages_parser.get_message_subject(), + messages_parser.get_message_text(), + messages_parser.get_thread_id() + ) + messages_parser.close() + except SQLException as ex: + #Error parsing Skype db + self._logger.log(Level.WARNING, + "Error parsing Skype database for message artifacts.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + except TskCoreException as ex: + #Severe error trying to add to case database.. case is not complete. + #These exceptions are thrown by the CommunicationArtifactsHelper. + self._logger.log(Level.SEVERE, + "Failed to add message artifacts to the case database.", ex) + self._logger.log(Level.SEVERE, traceback.format_exc()) + except BlackboardException as ex: + #Failed to post notification to blackboard + self._logger.log(Level.WARNING, + "Failed to post message artifact to the blackboard", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + +class SkypeCallLogsParser(TskCallLogsParser): + """ + Extracts TSK_CALLLOG information from the Skype database. + TSK_CALLLOG fields that are not in the Skype database are given + a default value inherited from the super class. + """ + + def __init__(self, calllog_db): + """ + Big picture: + The query below creates a contacts_list_with_groups table, which + represents the recipient info. A chatItem record holds ids for + both the recipient and sender. The first join onto chatItem fills + in the blanks for the recipients. The second join back onto person + handles the sender info. The result is a table with all of the + communication details. + + Implementation details: + - message_type w/ value 3 appeared to be the call type, regardless + of if it was audio or video. + + """ + super(SkypeCallLogsParser, self).__init__(calllog_db.runQuery( + """ + SELECT contacts_list_with_groups.conversation_id, + contacts_list_with_groups.participant_ids, + contacts_list_with_groups.participants, + time, + duration, + is_sender_me, + person_id as sender_id, + sender_name.name as sender_name + FROM (SELECT conversation_id, + Group_concat(person_id) AS participant_ids, + Group_concat("""+_format_user_name()+""") AS participants + FROM particiapnt AS PART + JOIN person AS P + ON PART.person_id = P.entry_id + GROUP BY conversation_id + UNION + SELECT entry_id, + NULL, + """+_format_user_name()+""" AS participant + FROM person) AS contacts_list_with_groups + JOIN chatitem AS C + ON C.conversation_link = contacts_list_with_groups.conversation_id + JOIN (SELECT entry_id as id, + """+_format_user_name()+""" AS name + FROM person + UNION + SELECT entry_id as id, + """+_format_user_name()+""" AS name + FROM user) AS sender_name + ON sender_name.id = C.person_id + WHERE message_type == 3 + """ + ) + ) + self._INCOMING_CALL_TYPE = 0 + self._OUTGOING_CALL_TYPE = 1 + + + def get_phone_number_from(self): + if self.get_call_direction() == self.INCOMING_CALL: + return Account.Address(self.result_set.getString("sender_id"), + self.result_set.getString("sender_name")) + + def get_phone_number_to(self): + if self.get_call_direction() == self.OUTGOING_CALL: + group_ids = self.result_set.getString("participant_ids") + name = self.result_set.getString("participants") + + if group_ids is not None: + group_ids = group_ids.split(",") + name = name.split(",") + recipients = [] + + for person_id, person_name in zip(group_ids, name): + recipients.append(Account.Address(person_id, person_name)) + + return recipients + + return Account.Address(self.result_set.getString("conversation_id"), name) + + return super(SkypeCallLogsParser, self).get_phone_number_to() + + def get_call_direction(self): + direction = self.result_set.getInt("is_sender_me") + if direction == self._INCOMING_CALL_TYPE: + return self.INCOMING_CALL + if direction == self._OUTGOING_CALL_TYPE: + return self.OUTGOING_CALL + return super(SkypeCallLogsParser, self).get_call_direction() + + def get_call_start_date_time(self): + return self.result_set.getLong("time") / 1000 + + def get_call_end_date_time(self): + start = self.get_call_start_date_time() + duration = self.result_set.getInt("duration") / 1000 + return start + duration + +class SkypeContactsParser(TskContactsParser): + """ + Extracts TSK_CONTACT information from the Skype database. + TSK_CONTACT fields that are not in the Skype database are given + a default value inherited from the super class. + """ + + def __init__(self, contact_db): + super(SkypeContactsParser, self).__init__(contact_db.runQuery( + """ + SELECT entry_id, + """+_format_user_name()+""" AS name + FROM person + """ + ) + ) + + def get_account_name(self): + return self.result_set.getString("entry_id") + + def get_contact_name(self): + return self.result_set.getString("name") + +class SkypeMessagesParser(TskMessagesParser): + """ + Extract TSK_MESSAGE information from the Skype database. + TSK_CONTACT fields that are not in the Skype database are given + a default value inherited from the super class. + """ + + def __init__(self, message_db): + """ + This query is very similar to the call logs query, the only difference is + it grabs more columns in the SELECT and excludes message_types which have + the call type value (3). + """ + super(SkypeMessagesParser, self).__init__(message_db.runQuery( + """ + SELECT contacts_list_with_groups.conversation_id, + contacts_list_with_groups.participant_ids, + contacts_list_with_groups.participants, + time, + content, + device_gallery_path, + is_sender_me, + person_id as sender_id, + sender_name.name AS sender_name + FROM (SELECT conversation_id, + Group_concat(person_id) AS participant_ids, + Group_concat("""+_format_user_name()+""") AS participants + FROM particiapnt AS PART + JOIN person AS P + ON PART.person_id = P.entry_id + GROUP BY conversation_id + UNION + SELECT entry_id as conversation_id, + NULL, + """+_format_user_name()+""" AS participant + FROM person) AS contacts_list_with_groups + JOIN chatitem AS C + ON C.conversation_link = contacts_list_with_groups.conversation_id + JOIN (SELECT entry_id as id, + """+_format_user_name()+""" AS name + FROM person + UNION + SELECT entry_id as id, + """+_format_user_name()+""" AS name + FROM user) AS sender_name + ON sender_name.id = C.person_id + WHERE message_type != 3 + """ + ) + ) + self._SKYPE_MESSAGE_TYPE = "Skype Message" + self._OUTGOING_MESSAGE_TYPE = 1 + self._INCOMING_MESSAGE_TYPE = 0 + + def get_message_type(self): + return self._SKYPE_MESSAGE_TYPE + + def get_phone_number_from(self): + if self.get_message_direction() == self.INCOMING: + return Account.Address(self.result_set.getString("sender_id"), + self.result_set.getString("sender_name")) + return super(SkypeMessagesParser, self).get_phone_number_from() + + def get_message_direction(self): + direction = self.result_set.getInt("is_sender_me") + if direction == self._OUTGOING_MESSAGE_TYPE: + return self.OUTGOING + if direction == self._INCOMING_MESSAGE_TYPE: + return self.INCOMING + return super(SkypeMessagesParser, self).get_message_direction() + + def get_phone_number_to(self): + if self.get_message_direction() == self.OUTGOING: + group_ids = self.result_set.getString("participant_ids") + names = self.result_set.getString("participants") + + if group_ids is not None: + group_ids = group_ids.split(",") + names = names.split(",") + recipients = [] + + for participant_id, participant_name in zip(group_ids, names): + recipients.append(Account.Address(participant_id, participant_name)) + + return recipients + + return Account.Address(self.result_set.getString("conversation_id"), names) + + return super(SkypeMessagesParser, self).get_phone_number_to() + + def get_message_date_time(self): + date = self.result_set.getLong("time") + return date / 1000 + + def get_message_text(self): + content = self.result_set.getString("content") + + if content is not None: + file_path = self.result_set.getString("device_gallery_path") + + #if a file name and file path are associated with a message, append it + if file_path is not None: + return general.appendAttachmentList(content, [file_path]) + + return content + + return super(SkypeMessagesParser, self).get_message_text() + + def get_thread_id(self): + group_ids = self.result_set.getString("participant_ids") + if group_ids is not None: + return self.result_set.getString("conversation_id") + return super(SkypeMessagesParser, self).get_thread_id() + +def _format_user_name(): + """ + This CASE SQL statement is used in many queries to + format the names of users. For a user, there is a first_name + column and a last_name column. Some of these columns can be null + and our goal is to produce the cleanest data possible. In the event + that both the first and last name columns are null, we return the skype_id + which is stored in the database as 'entry_id'. Commas are removed from the name + so that we can concatenate names into a comma seperate list for group chats. + """ + + return """ + CASE + WHEN Ifnull(first_name, "") == "" AND Ifnull(last_name, "") == "" THEN entry_id + WHEN first_name is NULL THEN replace(last_name, ",", "") + WHEN last_name is NULL THEN replace(first_name, ",", "") + ELSE replace(first_name, ",", "") || " " || replace(last_name, ",", "") + END + """ + + diff --git a/InternalPythonModules/android/textnow.py b/InternalPythonModules/android/textnow.py new file mode 100644 index 0000000000..216864f2e1 --- /dev/null +++ b/InternalPythonModules/android/textnow.py @@ -0,0 +1,392 @@ +""" +Autopsy Forensic Browser + +Copyright 2019 Basis Technology Corp. +Contact: carrier sleuthkit org + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from java.io import File +from java.lang import Class +from java.lang import ClassNotFoundException +from java.lang import Long +from java.lang import String +from java.sql import ResultSet +from java.sql import SQLException +from java.sql import Statement +from java.util.logging import Level +from java.util import ArrayList +from org.apache.commons.codec.binary import Base64 +from org.sleuthkit.autopsy.casemodule import Case +from org.sleuthkit.autopsy.coreutils import Logger +from org.sleuthkit.autopsy.coreutils import MessageNotifyUtil +from org.sleuthkit.autopsy.coreutils import AppSQLiteDB + +from org.sleuthkit.autopsy.datamodel import ContentUtils +from org.sleuthkit.autopsy.ingest import IngestJobContext +from org.sleuthkit.datamodel import AbstractFile +from org.sleuthkit.datamodel import BlackboardArtifact +from org.sleuthkit.datamodel import BlackboardAttribute +from org.sleuthkit.datamodel import Content +from org.sleuthkit.datamodel import TskCoreException +from org.sleuthkit.datamodel.Blackboard import BlackboardException +from org.sleuthkit.autopsy.casemodule import NoCurrentCaseException +from org.sleuthkit.datamodel import Account +from org.sleuthkit.datamodel.blackboardutils import CommunicationArtifactsHelper +from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import MessageReadStatus +from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import CommunicationDirection + +from TskMessagesParser import TskMessagesParser +from TskContactsParser import TskContactsParser +from TskCallLogsParser import TskCallLogsParser + +import traceback +import general + +class TextNowAnalyzer(general.AndroidComponentAnalyzer): + """ + Parses the TextNow App databases for TSK contacts, message + and calllog artifacts. + + The TextNow database in v6.41.0.2 is structured as follows: + - A messages table, which stores messages from/to a number + - A contacts table, which stores phone numbers + - A groups table, which stores each group the device owner is a part of + - A group_members table, which stores who is in each group + + The messages table contains both call logs and messages, with a type + column differentiating the two. + """ + + def __init__(self): + self._logger = Logger.getLogger(self.__class__.__name__) + self._TEXTNOW_PACKAGE_NAME = "com.enflick.android.TextNow" + self._PARSER_NAME = "TextNow Parser" + self._VERSION = "6.41.0.2" + + def analyze(self, dataSource, fileManager, context): + """ + Extract, Transform and Load all messages, contacts and + calllogs from the TextNow databases. + """ + + textnow_dbs = AppSQLiteDB.findAppDatabases(dataSource, + "textnow_data.db", True, self._TEXTNOW_PACKAGE_NAME) + + try: + for textnow_db in textnow_dbs: + current_case = Case.getCurrentCaseThrows() + helper = CommunicationArtifactsHelper( + current_case.getSleuthkitCase(), self._PARSER_NAME, + textnow_db.getDBFile(), Account.Type.TEXTNOW + ) + self.parse_contacts(textnow_db, helper) + self.parse_calllogs(textnow_db, helper) + self.parse_messages(textnow_db, helper) + except NoCurrentCaseException as ex: + self._logger.log(Level.WARNING, "No case currently open.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + + for textnow_db in textnow_dbs: + textnow_db.close() + + def parse_contacts(self, textnow_db, helper): + #Query for contacts and iterate row by row adding + #each contact artifact + try: + contacts_parser = TextNowContactsParser(textnow_db) + while contacts_parser.next(): + helper.addContact( + contacts_parser.get_account_name(), + contacts_parser.get_contact_name(), + contacts_parser.get_phone(), + contacts_parser.get_home_phone(), + contacts_parser.get_mobile_phone(), + contacts_parser.get_email() + ) + contacts_parser.close() + except SQLException as ex: + #Error parsing TextNow db + self._logger.log(Level.WARNING, "Error parsing TextNow databases for contacts", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + except TskCoreException as ex: + #Error adding artifacts to the case database.. case database is not complete. + self._logger.log(Level.SEVERE, + "Error adding TextNow contacts artifacts to the case database", ex) + self._logger.log(Level.SEVERE, traceback.format_exc()) + except BlackboardException as ex: + #Error posting notification to blackboard... + self._logger.log(Level.WARNING, + "Error posting TextNow contacts artifact to the blackboard", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + + def parse_calllogs(self, textnow_db, helper): + #Query for call logs and iterate row by row adding + #each call log artifact + try: + calllog_parser = TextNowCallLogsParser(textnow_db) + while calllog_parser.next(): + helper.addCalllog( + calllog_parser.get_call_direction(), + calllog_parser.get_phone_number_from(), + calllog_parser.get_phone_number_to(), + calllog_parser.get_call_start_date_time(), + calllog_parser.get_call_end_date_time(), + calllog_parser.get_call_type() + ) + calllog_parser.close() + except SQLException as ex: + self._logger.log(Level.WARNING, "Error parsing TextNow databases for calllogs", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + except TskCoreException as ex: + #Error adding artifacts to the case database.. case database is not complete. + self._logger.log(Level.SEVERE, + "Error adding TextNow call log artifacts to the case database", ex) + self._logger.log(Level.SEVERE, traceback.format_exc()) + except BlackboardException as ex: + #Error posting notification to blackboard... + self._logger.log(Level.WARNING, + "Error posting TextNow call log artifact to the blackboard", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + + def parse_messages(self, textnow_db, helper): + #Query for messages and iterate row by row adding + #each message artifact + try: + messages_parser = TextNowMessagesParser(textnow_db) + while messages_parser.next(): + helper.addMessage( + messages_parser.get_message_type(), + messages_parser.get_message_direction(), + messages_parser.get_phone_number_from(), + messages_parser.get_phone_number_to(), + messages_parser.get_message_date_time(), + messages_parser.get_message_read_status(), + messages_parser.get_message_subject(), + messages_parser.get_message_text(), + messages_parser.get_thread_id() + ) + messages_parser.close() + except SQLException as ex: + #Error parsing TextNow db + self._logger.log(Level.WARNING, "Error parsing TextNow databases for messages.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + except TskCoreException as ex: + #Error adding artifacts to the case database.. case database is not complete. + self._logger.log(Level.SEVERE, + "Error adding TextNow messages artifacts to the case database", ex) + self._logger.log(Level.SEVERE, traceback.format_exc()) + except BlackboardException as ex: + #Error posting notification to blackboard... + self._logger.log(Level.WARNING, + "Error posting TextNow messages artifact to the blackboard", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + +class TextNowCallLogsParser(TskCallLogsParser): + """ + Extracts TSK_CALLLOG information from the TextNow database. + TSK_CALLLOG fields that are not in the TextNow database are given + a default value inherited from the super class. + """ + + def __init__(self, calllog_db): + """ + message_type of 100 or 102 are for calls (audio, video) + """ + super(TextNowCallLogsParser, self).__init__(calllog_db.runQuery( + """ + SELECT contact_value AS num, + message_direction AS direction, + message_text AS duration, + date AS datetime + FROM messages AS M + WHERE message_type IN ( 100, 102 ) + """ + ) + ) + self._INCOMING_CALL_TYPE = 1 + self._OUTGOING_CALL_TYPE = 2 + + def get_phone_number_from(self): + if self.get_call_direction() == self.OUTGOING_CALL: + return super(TextNowCallLogsParser, self).get_phone_number_from() + return Account.Address(self.result_set.getString("num"), + self.result_set.getString("num")) + + def get_phone_number_to(self): + if self.get_call_direction() == self.INCOMING_CALL: + return super(TextNowCallLogsParser, self).get_phone_number_to() + return Account.Address(self.result_set.getString("num"), + self.result_set.getString("num")) + + def get_call_direction(self): + if self.result_set.getInt("direction") == self._INCOMING_CALL_TYPE: + return self.INCOMING_CALL + return self.OUTGOING_CALL + + def get_call_start_date_time(self): + return self.result_set.getLong("datetime") / 1000 + + def get_call_end_date_time(self): + start = self.get_call_start_date_time() + duration = self.result_set.getString("duration") + try: + return start + long(duration) + except ValueError as ve: + return super(TextNowCallLogsParser, self).get_call_end_date_time() + +class TextNowContactsParser(TskContactsParser): + """ + Extracts TSK_CONTACT information from the TextNow database. + TSK_CONTACT fields that are not in the TextNow database are given + a default value inherited from the super class. + """ + + def __init__(self, contact_db): + super(TextNowContactsParser, self).__init__(contact_db.runQuery( + """ + SELECT C.contact_value AS number, + CASE + WHEN contact_name IS NULL THEN contact_value + WHEN contact_name == "" THEN contact_value + ELSE contact_name + END name + FROM contacts AS C + """ + ) + ) + + def get_account_name(self): + return self.result_set.getString("number") + + def get_contact_name(self): + return self.result_set.getString("name") + + def get_phone(self): + return self.result_set.getString("number") + +class TextNowMessagesParser(TskMessagesParser): + """ + Extract TSK_MESSAGE information from the TextNow database. + TSK_CONTACT fields that are not in the TextNow database are given + a default value inherited from the super class. + """ + + def __init__(self, message_db): + """ + The query below does the following: + - The group_info inner query creates a comma seperated list of group recipients + for each group. This result is then joined on the groups table to get the thread id. + - The contacts table is unioned with this result so we have a complete map + of "from" phone_numbers -> recipients (group or single). This is the + 'to_from_map' inner query. + - Finally, the to_from_map results are joined with the messages table to get all + of the communication details. + """ + super(TextNowMessagesParser, self).__init__(message_db.runQuery( + """ + + SELECT CASE + WHEN message_direction == 2 THEN "" + WHEN to_addresses IS NULL THEN M.contact_value + ELSE contact_name + end from_address, + CASE + WHEN message_direction == 1 THEN "" + WHEN to_addresses IS NULL THEN M.contact_value + ELSE to_addresses + end to_address, + message_direction, + message_text, + M.READ, + M.date, + M.attach, + thread_id + FROM (SELECT group_info.contact_value, + group_info.to_addresses, + G.contact_value AS thread_id + FROM (SELECT GM.contact_value, + Group_concat(GM.member_contact_value) AS to_addresses + FROM group_members AS GM + GROUP BY GM.contact_value) AS group_info + JOIN groups AS G + ON G.contact_value = group_info.contact_value + UNION + SELECT c.contact_value, + NULL, + "-1" + FROM contacts AS c) AS to_from_map + JOIN messages AS M + ON M.contact_value = to_from_map.contact_value + WHERE message_type NOT IN ( 102, 100 ) + """ + ) + ) + self._TEXTNOW_MESSAGE_TYPE = "TextNow Message" + self._INCOMING_MESSAGE_TYPE = 1 + self._OUTGOING_MESSAGE_TYPE = 2 + self._UNKNOWN_THREAD_ID = "-1" + + def get_message_type(self): + return self._TEXTNOW_MESSAGE_TYPE + + def get_phone_number_from(self): + if self.result_set.getString("from_address") == "": + return super(TextNowMessagesParser, self).get_phone_number_from() + return Account.Address(self.result_set.getString("from_address"), + self.result_set.getString("from_address")) + + def get_message_direction(self): + direction = self.result_set.getInt("message_direction") + if direction == self._INCOMING_MESSAGE_TYPE: + return self.INCOMING + return self.OUTGOING + + def get_phone_number_to(self): + if self.result_set.getString("to_address") == "": + return super(TextNowMessagesParser, self).get_phone_number_to() + recipients = self.result_set.getString("to_address").split(",") + + recipient_accounts = [] + for recipient in recipients: + recipient_accounts.append(Account.Address(recipient, recipient)) + + return recipient_accounts + + def get_message_date_time(self): + #convert ms to s + return self.result_set.getLong("date") / 1000; + + def get_message_read_status(self): + read = self.result_set.getBoolean("read") + if self.get_message_direction() == self.INCOMING: + if read == True: + return self.READ + return self.UNREAD + + #read status for outgoing messages cannot be determined, give default + return super(TextNowMessagesParser, self).get_message_read_status() + + def get_message_text(self): + text = self.result_set.getString("message_text") + attachment = self.result_set.getString("attach") + if attachment != "": + text = general.appendAttachmentList(text, [attachment]) + return text + + def get_thread_id(self): + thread_id = self.result_set.getString("thread_id") + if thread_id == self._UNKNOWN_THREAD_ID: + return super(TextNowMessagesParser, self).get_thread_id() + return thread_id diff --git a/InternalPythonModules/android/viber.py b/InternalPythonModules/android/viber.py new file mode 100644 index 0000000000..6a7e4b2451 --- /dev/null +++ b/InternalPythonModules/android/viber.py @@ -0,0 +1,376 @@ +""" +Autopsy Forensic Browser + +Copyright 2019 Basis Technology Corp. +Contact: carrier sleuthkit org + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" +from java.io import File +from java.lang import Class +from java.lang import ClassNotFoundException +from java.lang import Long +from java.lang import String +from java.sql import ResultSet +from java.sql import SQLException +from java.sql import Statement +from java.util.logging import Level +from java.util import ArrayList +from org.apache.commons.codec.binary import Base64 +from org.sleuthkit.autopsy.casemodule import Case +from org.sleuthkit.autopsy.coreutils import Logger +from org.sleuthkit.autopsy.coreutils import MessageNotifyUtil +from org.sleuthkit.autopsy.coreutils import AppSQLiteDB + +from org.sleuthkit.autopsy.datamodel import ContentUtils +from org.sleuthkit.autopsy.ingest import IngestJobContext +from org.sleuthkit.datamodel import AbstractFile +from org.sleuthkit.datamodel import BlackboardArtifact +from org.sleuthkit.datamodel import BlackboardAttribute +from org.sleuthkit.datamodel import Content +from org.sleuthkit.datamodel import TskCoreException +from org.sleuthkit.datamodel.Blackboard import BlackboardException +from org.sleuthkit.autopsy.casemodule import NoCurrentCaseException +from org.sleuthkit.datamodel import Account +from org.sleuthkit.datamodel.blackboardutils import CommunicationArtifactsHelper +from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import MessageReadStatus +from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import CommunicationDirection +from TskMessagesParser import TskMessagesParser +from TskContactsParser import TskContactsParser +from TskCallLogsParser import TskCallLogsParser + +import traceback +import general + +class ViberAnalyzer(general.AndroidComponentAnalyzer): + """ + Parses the Viber App databases for TSK contacts, message + and calllog artifacts. + + The Viber v11.5.0 database structure is as follows: + - People can take part in N conversation(s). A conversation can have M + members and messages are exchanged in a conversation. + - Viber has a conversation table, a participant table (the people/members in the above + analogy) and a messages table. + - Each row of the participants table maps a person to a conversation_id + - Each row in the messages table has a from participant id and a conversation id. + """ + + def __init__(self): + self._logger = Logger.getLogger(self.__class__.__name__) + self._VIBER_PACKAGE_NAME = "com.viber.voip" + self._PARSER_NAME = "Viber Parser" + self._VERSION = "11.5.0" + + def analyze(self, dataSource, fileManager, context): + """ + Extract, Transform and Load all messages, contacts and + calllogs from the Viber databases. + """ + + try: + contact_and_calllog_dbs = AppSQLiteDB.findAppDatabases(dataSource, + "viber_data", True, self._VIBER_PACKAGE_NAME) + message_dbs = AppSQLiteDB.findAppDatabases(dataSource, + "viber_messages", True, self._VIBER_PACKAGE_NAME) + + #Extract TSK_CONTACT and TSK_CALLLOG information + for contact_and_calllog_db in contact_and_calllog_dbs: + current_case = Case.getCurrentCaseThrows() + helper = CommunicationArtifactsHelper( + current_case.getSleuthkitCase(), self._PARSER_NAME, + contact_and_calllog_db.getDBFile(), Account.Type.VIBER) + self.parse_contacts(contact_and_calllog_db, helper) + self.parse_calllogs(contact_and_calllog_db, helper) + + #Extract TSK_MESSAGE information + for message_db in message_dbs: + current_case = Case.getCurrentCaseThrows() + helper = CommunicationArtifactsHelper( + current_case.getSleuthkitCase(), self._PARSER_NAME, + message_db.getDBFile(), Account.Type.VIBER) + self.parse_messages(message_db, helper) + + except NoCurrentCaseException as ex: + self._logger.log(Level.WARNING, "No case currently open.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + + for message_db in messages_db: + message_db.close() + + for contact_and_calllog_db in contact_and_calllog_dbs: + contact_and_calllog_db.close() + + def parse_contacts(self, contacts_db, helper): + try: + contacts_parser = ViberContactsParser(contacts_db) + while contacts_parser.next(): + helper.addContact( + contacts_parser.get_account_name(), + contacts_parser.get_contact_name(), + contacts_parser.get_phone(), + contacts_parser.get_home_phone(), + contacts_parser.get_mobile_phone(), + contacts_parser.get_email() + ) + contacts_parser.close() + except SQLException as ex: + self._logger.log(Level.WARNING, "Error querying the viber database for contacts.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + except TskCoreException as ex: + self._logger.log(Level.SEVERE, + "Error adding viber contacts artifact to case database.", ex) + self._logger.log(Level.SEVERE, traceback.format_exc()) + except BlackboardException as ex: + self._logger.log(Level.WARNING, + "Error posting viber contacts artifact to the blackboard.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + + def parse_calllogs(self, calllogs_db, helper): + try: + calllog_parser = ViberCallLogsParser(calllogs_db) + while calllog_parser.next(): + helper.addCalllog( + calllog_parser.get_call_direction(), + calllog_parser.get_phone_number_from(), + calllog_parser.get_phone_number_to(), + calllog_parser.get_call_start_date_time(), + calllog_parser.get_call_end_date_time(), + calllog_parser.get_call_type() + ) + calllog_parser.close() + except SQLException as ex: + self._logger.log(Level.WARNING, "Error querying the viber database for calllogs.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + except TskCoreException as ex: + self._logger.log(Level.SEVERE, + "Error adding viber calllogs artifact to case database.", ex) + self._logger.log(Level.SEVERE, traceback.format_exc()) + except BlackboardException as ex: + self._logger.log(Level.WARNING, + "Error posting viber calllogs artifact to the blackboard.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + + + def parse_messages(self, messages_db, helper): + try: + messages_parser = ViberMessagesParser(messages_db) + while messages_parser.next(): + helper.addMessage( + messages_parser.get_message_type(), + messages_parser.get_message_direction(), + messages_parser.get_phone_number_from(), + messages_parser.get_phone_number_to(), + messages_parser.get_message_date_time(), + messages_parser.get_message_read_status(), + messages_parser.get_message_subject(), + messages_parser.get_message_text(), + messages_parser.get_thread_id() + ) + messages_parser.close() + except SQLException as ex: + self._logger.log(Level.WARNING, "Error querying the viber database for messages.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + except TskCoreException as ex: + self._logger.log(Level.SEVERE, + "Error adding viber messages artifact to case database.", ex) + self._logger.log(Level.SEVERE, traceback.format_exc()) + except BlackboardException as ex: + self._logger.log(Level.WARNING, + "Error posting viber messages artifact to the blackboard.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + +class ViberCallLogsParser(TskCallLogsParser): + """ + Extracts TSK_CALLLOG information from the Viber database. + TSK_CALLLOG fields that are not in the Viber database are given + a default value inherited from the super class. + """ + + def __init__(self, calllog_db): + super(ViberCallLogsParser, self).__init__(calllog_db.runQuery( + """ + SELECT C.canonized_number AS number, + C.type AS direction, + C.duration AS seconds, + C.date AS start_time, + C.viber_call_type AS call_type + FROM calls AS C + """ + ) + ) + + self._OUTGOING_CALL_TYPE = 2 + self._INCOMING_CALL_TYPE = 1 + self._MISSED_CALL_TYPE = 3 + self._AUDIO_CALL_TYPE = 1 + self._VIDEO_CALL_TYPE = 4 + + def get_phone_number_from(self): + if self.get_call_direction() == self.INCOMING_CALL: + return Account.Address(self.result_set.getString("number"), + self.result_set.getString("number")) + #Give default value if the call is outgoing, + #the device's # is not stored in the database. + return super(ViberCallLogsParser, self).get_phone_number_from() + + def get_phone_number_to(self): + if self.get_call_direction() == self.OUTGOING_CALL: + return Account.Address(self.result_set.getString("number"), + self.result_set.getString("number")) + #Give default value if the call is incoming, + #the device's # is not stored in the database. + return super(ViberCallLogsParser, self).get_phone_number_to() + + def get_call_direction(self): + direction = self.result_set.getInt("direction") + if direction == self._INCOMING_CALL_TYPE or direction == self._MISSED_CALL_TYPE: + return self.INCOMING_CALL + return self.OUTGOING_CALL + + def get_call_start_date_time(self): + return self.result_set.getLong("start_time") / 1000 + + def get_call_end_date_time(self): + start_time = self.get_call_start_date_time() + duration = self.result_set.getLong("seconds") + return start_time + duration + + def get_call_type(self): + call_type = self.result_set.getInt("call_type") + if call_type == self._AUDIO_CALL_TYPE: + return self.AUDIO_CALL + if call_type == self._VIDEO_CALL_TYPE: + return self.VIDEO_CALL + return super(ViberCallLogsParser, self).get_call_type() + +class ViberContactsParser(TskContactsParser): + """ + Extracts TSK_CONTACT information from the Viber database. + TSK_CONTACT fields that are not in the Viber database are given + a default value inherited from the super class. + """ + + def __init__(self, contact_db): + super(ViberContactsParser, self).__init__(contact_db.runQuery( + """ + SELECT C.display_name AS name, + D.data2 AS number + FROM phonebookcontact AS C + JOIN phonebookdata AS D + ON C._id = D.contact_id + """ + ) + ) + + def get_account_name(self): + return self.result_set.getString("number") + + def get_contact_name(self): + return self.result_set.getString("name") + + def get_phone(self): + return self.result_set.getString("number") + +class ViberMessagesParser(TskMessagesParser): + """ + Extract TSK_MESSAGE information from the Viber database. + TSK_CONTACT fields that are not in the Viber database are given + a default value inherited from the super class. + """ + + def __init__(self, message_db): + """ + The query below does the following: + - The first two inner joins on participants and participants_info build + the 1 to many (M) mappings between the sender and the recipients for each + conversation_id. If a and b do private messaging, then 2 rows in the result + will be a -> b and b -> a. + If a, b, c, d are in a group, then 4 rows containing a -> b,c,d. b -> a,c,d. etc. + Participants_info is needed to get phone numbers. + - The result of the above step is a look up table for each message. Joining this result + onto the messages table lets us know which participant a message originated from and + everyone else that received it. + """ + super(ViberMessagesParser, self).__init__(message_db.runQuery( + """ + SELECT convo_participants.from_number AS from_number, + convo_participants.recipients AS recipients, + M.conversation_id AS thread_id, + M.body AS msg_content, + M.send_type AS direction, + M.msg_date AS msg_date, + M.unread AS read_status + FROM (SELECT *, + group_concat(TO_RESULT.number) AS recipients + FROM (SELECT P._id AS FROM_ID, + P.conversation_id, + PI.number AS FROM_NUMBER + FROM participants AS P + JOIN participants_info AS PI + ON P.participant_info_id = PI._id) AS FROM_RESULT + JOIN (SELECT P._id AS TO_ID, + P.conversation_id, + PI.number + FROM participants AS P + JOIN participants_info AS PI + ON P.participant_info_id = PI._id) AS TO_RESULT + ON FROM_RESULT.from_id != TO_RESULT.to_id + AND FROM_RESULT.conversation_id = TO_RESULT.conversation_id + GROUP BY FROM_RESULT.from_id) AS convo_participants + JOIN messages AS M + ON M.participant_id = convo_participants.from_id + AND M.conversation_id = convo_participants.conversation_id + """ + ) + ) + self._VIBER_MESSAGE_TYPE = "Viber Message" + self._INCOMING_MESSAGE_TYPE = 0 + self._OUTGOING_MESSAGE_TYPE = 1 + + def get_message_type(self): + return self._VIBER_MESSAGE_TYPE + + def get_phone_number_from(self): + return Account.Address(self.result_set.getString("from_number"), + self.result_set.getString("from_number")) + + def get_message_direction(self): + direction = self.result_set.getInt("direction") + if direction == self._INCOMING_MESSAGE_TYPE: + return self.INCOMING + return self.OUTGOING + + def get_phone_number_to(self): + recipients = [] + for token in self.result_set.getString("recipients").split(","): + recipients.append(Account.Address(token, token)) + return recipients + + def get_message_date_time(self): + #transform from ms to seconds + return self.result_set.getLong("msg_date") / 1000 + + def get_message_read_status(self): + if self.get_message_direction() == self.INCOMING: + if self.result_set.getInt("read_status") == 0: + return self.READ + else: + return self.UNREAD + return super(ViberMessagesParser, self).get_message_read_status() + + def get_message_text(self): + return self.result_set.getString("msg_content") + + def get_thread_id(self): + return str(self.result_set.getInt("thread_id")) diff --git a/InternalPythonModules/android/whatsapp.py b/InternalPythonModules/android/whatsapp.py new file mode 100644 index 0000000000..328f371c76 --- /dev/null +++ b/InternalPythonModules/android/whatsapp.py @@ -0,0 +1,458 @@ +""" +Autopsy Forensic Browser + +Copyright 2019 Basis Technology Corp. +Contact: carrier sleuthkit org + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" +from java.io import File +from java.lang import Class +from java.lang import ClassNotFoundException +from java.lang import Long +from java.lang import String +from java.sql import ResultSet +from java.sql import SQLException +from java.sql import Statement +from java.util.logging import Level +from java.util import ArrayList +from org.apache.commons.codec.binary import Base64 +from org.sleuthkit.autopsy.casemodule import Case +from org.sleuthkit.autopsy.coreutils import Logger +from org.sleuthkit.autopsy.coreutils import MessageNotifyUtil +from org.sleuthkit.autopsy.coreutils import AppSQLiteDB + +from org.sleuthkit.autopsy.datamodel import ContentUtils +from org.sleuthkit.autopsy.ingest import IngestJobContext +from org.sleuthkit.datamodel import AbstractFile +from org.sleuthkit.datamodel import BlackboardArtifact +from org.sleuthkit.datamodel import BlackboardAttribute +from org.sleuthkit.datamodel import Content +from org.sleuthkit.datamodel import TskCoreException +from org.sleuthkit.datamodel.Blackboard import BlackboardException +from org.sleuthkit.autopsy.casemodule import NoCurrentCaseException +from org.sleuthkit.datamodel import Account +from org.sleuthkit.datamodel.blackboardutils import CommunicationArtifactsHelper +from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import MessageReadStatus +from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import CommunicationDirection +from TskMessagesParser import TskMessagesParser +from TskContactsParser import TskContactsParser +from TskCallLogsParser import TskCallLogsParser + +import traceback +import general + +class WhatsAppAnalyzer(general.AndroidComponentAnalyzer): + """ + Parses the WhatsApp databases for TSK contact, message + and calllog artifacts. + """ + + def __init__(self): + self._logger = Logger.getLogger(self.__class__.__name__) + self._WHATSAPP_PACKAGE_NAME = "com.whatsapp" + self._PARSER_NAME = "WhatsApp Parser" + self._VERSION = "2.19.244" + + def analyze(self, dataSource, fileManager, context): + """ + Extract, Transform and Load all TSK contact, message + and calllog artifacts from the WhatsApp databases. + """ + + try: + contact_dbs = AppSQLiteDB.findAppDatabases(dataSource, + "wa.db", True, self._WHATSAPP_PACKAGE_NAME) + calllog_and_message_dbs = AppSQLiteDB.findAppDatabases(dataSource, + "msgstore.db", True, self._WHATSAPP_PACKAGE_NAME) + + #Extract TSK_CONTACT information + for contact_db in contact_dbs: + current_case = Case.getCurrentCaseThrows() + helper = CommunicationArtifactsHelper( + current_case.getSleuthkitCase(), self._PARSER_NAME, + contact_db.getDBFile(), Account.Type.WHATSAPP) + self.parse_contacts(contact_db, helper) + + for calllog_and_message_db in calllog_and_message_dbs: + current_case = Case.getCurrentCaseThrows() + helper = CommunicationArtifactsHelper( + current_case.getSleuthkitCase(), self._PARSER_NAME, + calllog_and_message_db.getDBFile(), Account.Type.WHATSAPP) + self.parse_calllogs(calllog_and_message_db, helper) + self.parse_messages(dataSource, calllog_and_message_db, helper) + + except NoCurrentCaseException as ex: + #If there is no current case, bail out immediately. + self._logger.log(Level.WARNING, "No case currently open.", ex) + self._logger.log(Level.WARNING, traceback.format_exec()) + + #Clean up open file handles. + for contact_db in contact_dbs: + contact_db.close() + + for calllog_and_message_db in calllog_and_message_dbs: + calllog_and_message_db.close() + + def parse_contacts(self, contacts_db, helper): + try: + contacts_parser = WhatsAppContactsParser(contacts_db) + while contacts_parser.next(): + helper.addContact( + contacts_parser.get_account_name(), + contacts_parser.get_contact_name(), + contacts_parser.get_phone(), + contacts_parser.get_home_phone(), + contacts_parser.get_mobile_phone(), + contacts_parser.get_email() + ) + contacts_parser.close() + except SQLException as ex: + self._logger.log(Level.WARNING, "Error querying the whatsapp database for contacts.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + except TskCoreException as ex: + self._logger.log(Level.SEVERE, + "Error adding whatsapp contact artifacts to the case database.", ex) + self._logger.log(Level.SEVERE, traceback.format_exc()) + except BlackboardException as ex: + self._logger.log(Level.WARNING, + "Error posting contact artifact to the blackboard.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + + def parse_calllogs(self, calllogs_db, helper): + try: + single_calllogs_parser = WhatsAppSingleCallLogsParser(calllogs_db) + while single_calllogs_parser.next(): + helper.addCalllog( + single_calllogs_parser.get_call_direction(), + single_calllogs_parser.get_phone_number_from(), + single_calllogs_parser.get_phone_number_to(), + single_calllogs_parser.get_call_start_date_time(), + single_calllogs_parser.get_call_end_date_time(), + single_calllogs_parser.get_call_type() + ) + single_calllogs_parser.close() + + group_calllogs_parser = WhatsAppGroupCallLogsParser(calllogs_db) + while group_calllogs_parser.next(): + helper.addCalllog( + group_calllogs_parser.get_call_direction(), + group_calllogs_parser.get_phone_number_from(), + group_calllogs_parser.get_phone_number_to(), + group_calllogs_parser.get_call_start_date_time(), + group_calllogs_parser.get_call_end_date_time(), + group_calllogs_parser.get_call_type() + ) + group_calllogs_parser.close() + except SQLException as ex: + self._logger.log(Level.WARNING, "Error querying the whatsapp database for calllogs.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + except TskCoreException as ex: + self._logger.log(Level.SEVERE, + "Error adding whatsapp calllog artifacts to the case database.", ex) + self._logger.log(Level.SEVERE, traceback.format_exc()) + except BlackboardException as ex: + self._logger.log(Level.WARNING, + "Error posting calllog artifact to the blackboard.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + + def parse_messages(self, dataSource, messages_db, helper): + try: + messages_db.attachDatabase(dataSource, "wa.db", + messages_db.getDBFile().getParentPath(), "wadb") + + messages_parser = WhatsAppMessagesParser(messages_db) + while messages_parser.next(): + helper.addMessage( + messages_parser.get_message_type(), + messages_parser.get_message_direction(), + messages_parser.get_phone_number_from(), + messages_parser.get_phone_number_to(), + messages_parser.get_message_date_time(), + messages_parser.get_message_read_status(), + messages_parser.get_message_subject(), + messages_parser.get_message_text(), + messages_parser.get_thread_id() + ) + messages_parser.close() + except SQLException as ex: + self._logger.log(Level.WARNING, "Error querying the whatsapp database for contacts.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + except TskCoreException as ex: + self._logger.log(Level.SEVERE, + "Error adding whatsapp contact artifacts to the case database.", ex) + self._logger.log(Level.SEVERE, traceback.format_exc()) + except BlackboardException as ex: + self._logger.log(Level.WARNING, + "Error posting contact artifact to the blackboard.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + +class WhatsAppGroupCallLogsParser(TskCallLogsParser): + """ + Extracts TSK_CALLLOG information from group call logs + in the WhatsApp database. + """ + + def __init__(self, calllog_db): + super(WhatsAppGroupCallLogsParser, self).__init__(calllog_db.runQuery( + """ + SELECT CL.video_call, + CL.timestamp, + CL.duration, + CL.from_me, + J1.raw_string AS from_id, + group_concat(J.raw_string) AS group_members + FROM call_log_participant_v2 AS CLP + JOIN call_log AS CL + ON CL._id = CLP.call_log_row_id + JOIN jid AS J + ON J._id = CLP.jid_row_id + JOIN jid as J1 + ON J1._id = CL.jid_row_id + GROUP BY CL._id + """ + ) + ) + self._INCOMING_CALL_TYPE = 0 + self._OUTGOING_CALL_TYPE = 1 + self._VIDEO_CALL_TYPE = 1 + + def get_call_direction(self): + if self.result_set.getInt("from_me") == self._INCOMING_CALL_TYPE: + return self.INCOMING_CALL + return self.OUTGOING_CALL + + def get_phone_number_from(self): + if self.get_call_direction() == self.INCOMING_CALL: + sender = self.result_set.getString("from_id") + return Account.Address(sender, sender) + return super(WhatsAppGroupCallLogsParser, self).get_phone_number_from() + + def get_phone_number_to(self): + if self.get_call_direction() == self.OUTGOING_CALL: + group = self.result_set.getString("group_members") + members = [] + for token in group.split(","): + members.append(Account.Address(token, token)) + return members + return super(WhatsAppGroupCallLogsParser, self).get_phone_number_to() + + def get_call_start_date_time(self): + return self.result_set.getLong("timestamp") / 1000 + + def get_call_end_date_time(self): + start = self.get_call_start_date_time() + duration = self.result_set.getInt("duration") + return start + duration + + def get_call_type(self): + if self.result_set.getInt("video_call") == self._VIDEO_CALL_TYPE: + return self.VIDEO_CALL + return self.AUDIO_CALL + +class WhatsAppSingleCallLogsParser(TskCallLogsParser): + """ + Extracts TSK_CALLLOG information from 1 to 1 call logs + in the WhatsApp database. + """ + + def __init__(self, calllog_db): + super(WhatsAppSingleCallLogsParser, self).__init__(calllog_db.runQuery( + """ + SELECT CL.timestamp, + CL.video_call, + CL.duration, + J.raw_string AS num, + CL.from_me + FROM call_log AS CL + JOIN jid AS J + ON J._id = CL.jid_row_id + WHERE CL._id NOT IN (SELECT DISTINCT call_log_row_id + FROM call_log_participant_v2) + """ + ) + ) + self._INCOMING_CALL_TYPE = 0 + self._OUTGOING_CALL_TYPE = 1 + self._VIDEO_CALL_TYPE = 1 + + def get_call_direction(self): + if self.result_set.getInt("from_me") == self._INCOMING_CALL_TYPE: + return self.INCOMING_CALL + return self.OUTGOING_CALL + + def get_phone_number_from(self): + if self.get_call_direction() == self.INCOMING_CALL: + sender = self.result_set.getString("num") + return Account.Address(sender, sender) + return super(WhatsAppSingleCallLogsParser, self).get_phone_number_from() + + def get_phone_number_to(self): + if self.get_call_direction() == self.OUTGOING_CALL: + to = self.result_set.getString("num") + return Account.Address(to, to) + return super(WhatsAppSingleCallLogsParser, self).get_phone_number_to() + + def get_call_start_date_time(self): + return self.result_set.getLong("timestamp") / 1000 + + def get_call_end_date_time(self): + start = self.get_call_start_date_time() + duration = self.result_set.getInt("duration") + return start + duration + + def get_call_type(self): + if self.result_set.getInt("video_call") == self._VIDEO_CALL_TYPE: + return self.VIDEO_CALL + return self.AUDIO_CALL + + +class WhatsAppContactsParser(TskContactsParser): + """ + Extracts TSK_CONTACT information from the WhatsApp database. + TSK_CONTACT fields that are not in the WhatsApp database are given + a default value inherited from the super class. + """ + + def __init__(self, contact_db): + super(WhatsAppContactsParser, self).__init__(contact_db.runQuery( + """ + SELECT jid, + CASE + WHEN WC.number IS NULL THEN WC.jid + WHEN WC.number == "" THEN WC.jid + ELSE WC.number + END number, + CASE + WHEN WC.given_name IS NULL + AND WC.family_name IS NULL + AND WC.display_name IS NULL THEN WC.jid + WHEN WC.given_name IS NULL + AND WC.family_name IS NULL THEN WC.display_name + WHEN WC.given_name IS NULL THEN WC.family_name + WHEN WC.family_name IS NULL THEN WC.given_name + ELSE WC.given_name + || " " + || WC.family_name + END name + FROM wa_contacts AS WC + """ + ) + ) + + def get_account_name(self): + return self.result_set.getString("jid") + + def get_contact_name(self): + return self.result_set.getString("name") + + def get_phone(self): + return self.result_set.getString("number") + +class WhatsAppMessagesParser(TskMessagesParser): + """ + Extract TSK_MESSAGE information from the WhatsApp database. + TSK_CONTACT fields that are not in the WhatsApp database are given + a default value inherited from the super class. + """ + + def __init__(self, message_db): + super(WhatsAppMessagesParser, self).__init__(message_db.runQuery( + """ + SELECT M.key_remote_jid AS id, + contact_info.recipients, + key_from_me AS direction, + CASE + WHEN M.data IS NULL THEN "" + ELSE M.data + END AS content, + M.timestamp AS send_timestamp, + M.received_timestamp, + M.remote_resource AS group_sender, + M.media_url As attachment + FROM (SELECT jid, + recipients + FROM wadb.wa_contacts AS WC + LEFT JOIN (SELECT gjid, + group_concat(CASE + WHEN jid == "" THEN NULL + ELSE jid + END) AS recipients + FROM group_participants + GROUP BY gjid) AS group_map + ON WC.jid = group_map.gjid + GROUP BY jid) AS contact_info + JOIN messages AS M + ON M.key_remote_jid = contact_info.jid + """ + ) + ) + self._WHATSAPP_MESSAGE_TYPE = "WhatsApp Message" + self._INCOMING_MESSAGE_TYPE = 0 + self._OUTGOING_MESSAGE_TYPE = 1 + self._message_db = message_db + + def get_message_type(self): + return self._WHATSAPP_MESSAGE_TYPE + + def get_phone_number_to(self): + if self.get_message_direction() == self.OUTGOING: + group = self.result_set.getString("recipients") + if group is not None: + group = group.split(",") + + recipients = [] + for token in group: + recipients.append(Account.Address(token, token)) + + return recipients + + return Account.Address(self.result_set.getString("id"), + self.result_set.getString("id")) + return super(WhatsAppMessagesParser, self).get_phone_number_to() + + def get_phone_number_from(self): + if self.get_message_direction() == self.INCOMING: + group_sender = self.result_set.getString("group_sender") + group = self.result_set.getString("recipients") + if group_sender is not None and group is not None: + return Account.Address(group_sender, group_sender) + else: + return Account.Address(self.result_set.getString("id"), + self.result_set.getString("id")) + return super(WhatsAppMessagesParser, self).get_phone_number_from() + + def get_message_direction(self): + direction = self.result_set.getInt("direction") + if direction == self._INCOMING_MESSAGE_TYPE: + return self.INCOMING + return self.OUTGOING + + def get_message_date_time(self): + #transform from ms to seconds + if self.get_message_direction() == self.OUTGOING: + return self.result_set.getLong("send_timestamp") / 1000 + return self.result_set.getLong("received_timestamp") / 1000 + + def get_message_text(self): + message = self.result_set.getString("content") + attachment = self.result_set.getString("attachment") + if attachment is not None: + return general.appendAttachmentList(message, [attachment]) + return message + + def get_thread_id(self): + group = self.result_set.getString("recipients") + if group is not None: + return self.result_set.getString("id") + return super(WhatsAppMessagesParser, self).get_thread_id() diff --git a/InternalPythonModules/android/xender.py b/InternalPythonModules/android/xender.py index e3c72f33e2..cdc520fb11 100644 --- a/InternalPythonModules/android/xender.py +++ b/InternalPythonModules/android/xender.py @@ -29,6 +29,7 @@ from java.util.logging import Level from java.util import ArrayList from org.apache.commons.codec.binary import Base64 from org.sleuthkit.autopsy.casemodule import Case +from org.sleuthkit.autopsy.casemodule import NoCurrentCaseException from org.sleuthkit.autopsy.coreutils import Logger from org.sleuthkit.autopsy.coreutils import MessageNotifyUtil from org.sleuthkit.autopsy.coreutils import AppSQLiteDB diff --git a/InternalPythonModules/android/zapya.py b/InternalPythonModules/android/zapya.py index 672795c076..230405075d 100644 --- a/InternalPythonModules/android/zapya.py +++ b/InternalPythonModules/android/zapya.py @@ -29,6 +29,7 @@ from java.util.logging import Level from java.util import ArrayList from org.apache.commons.codec.binary import Base64 from org.sleuthkit.autopsy.casemodule import Case +from org.sleuthkit.autopsy.casemodule import NoCurrentCaseException from org.sleuthkit.autopsy.coreutils import Logger from org.sleuthkit.autopsy.coreutils import MessageNotifyUtil from org.sleuthkit.autopsy.coreutils import AppSQLiteDB