Fully implemented text now parser

This commit is contained in:
U-BASIS\dsmyda
2019-09-13 17:47:14 -04:00
parent e90a2ea15c
commit 8f3eab11a4
+131 -5
View File
@@ -41,6 +41,7 @@ from org.sleuthkit.datamodel import Account
from TskMessagesParser import TskMessagesParser
from TskContactsParser import TskContactsParser
from TskCallLogsParser import TskCallLogsParser
from general import appendAttachmentList
import traceback
import general
@@ -55,6 +56,7 @@ class TextNowAnalyzer(general.AndroidComponentAnalyzer):
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):
"""
@@ -64,13 +66,13 @@ class TextNowAnalyzer(general.AndroidComponentAnalyzer):
try:
textnow_dbs = AppSQLiteDB.findAppDatabases(dataSource,
"viber_data", True, self._TEXTNOW_PACKAGE_NAME)
"textnow_data.db", True, self._TEXTNOW_PACKAGE_NAME)
#Extract TSK_CONTACT and TSK_CALLLOG information
for textnow_db in textnow_dbs:
helper = AppDBParserHelper(self._PARSER_NAME,
textnow_db.getDBFile(), Account.Type.TEXTNOW)
#Extract TSK_CONTACT information
contacts_parser = TextNowContactsParser(textnow_db)
while contacts_parser.next():
helper.addContact(
@@ -83,6 +85,7 @@ class TextNowAnalyzer(general.AndroidComponentAnalyzer):
)
contacts_parser.close()
#Extract TSK_CALLLOG information
calllog_parser = TextNowCallLogsParser(textnow_db)
while calllog_parser.next():
helper.addCalllog(
@@ -95,6 +98,7 @@ class TextNowAnalyzer(general.AndroidComponentAnalyzer):
)
calllog_parser.close()
#Extract TSK_MESSAGES information
messages_parser = TextNowMessagesParser(textnow_db)
while messages_parser.next():
helper.addMessage(
@@ -109,7 +113,7 @@ class TextNowAnalyzer(general.AndroidComponentAnalyzer):
messages_parser.get_thread_id()
)
messages_parser.close()
textnow_db.close()
except (SQLException, TskCoreException) as ex:
#Error parsing TextNow db
@@ -126,21 +130,47 @@ class TextNowCallLogsParser(TskCallLogsParser):
def __init__(self, calllog_db):
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
self._has_errors = False
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):
def get_call_type(self):
start = self.get_call_start_date_time()
duration = self.result_set.getString("duration")
try:
return start + long(duration)
except ValueError as ve:
self._has_errors = True
return super(TextNowCallLogsParser, self).get_call_end_date_time()
class TextNowContactsParser(TskContactsParser):
"""
@@ -152,15 +182,25 @@ class TextNowContactsParser(TskContactsParser):
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):
"""
@@ -170,26 +210,112 @@ class TextNowMessagesParser(TskMessagesParser):
"""
def __init__(self, message_db):
"""
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.
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._id 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()
return Account.Address(self.result_set.getString("to_address"),
self.result_set.getString("to_address"))
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 = appendAttachmentList(text, [attachment])
return text
def get_thread_id(self):
thread_id = self.result_set.getInt("thread_id")
if thread_id == self._UNKNOWN_THREAD_ID:
return super(TextNowMessagesParser, self).get_thread_id()
return str(thread_id)