tweaselORG / appstraction

An abstraction layer for common instrumentation functions (e.g. installing and starting apps, setting preferences, etc.) on Android and iOS.
MIT License
4 stars 1 forks source link

Abstract system APIs for common data stores to place honey data #123

Open zner0L opened 6 months ago

zner0L commented 6 months ago

System APIs that should be supported:

Both:

iOS:

zner0L commented 6 months ago

For iOS, creating events could be done using the Events and Reminders API

On Android, you might be able to send an Intent: https://stackoverflow.com/questions/7160231/how-to-add-a-calendar-event-on-an-android-device-with-a-given-date

zner0L commented 6 months ago

On Android, you can trigger many actions using common intents: https://developer.android.com/guide/components/intents-common

You can trigger these events using am start -a android.intent.action.INSERT -t <type of data> -e <extras>. This sometimes requires user interaction.

For the calendar, this looks like this:

adb shell "am start -a android.intent.action.INSERT -t 'vnd.android.cursor.dir/event' -e beginTime 1702381968000 -e allDay true -e title 'Test Event' -e endTime 1705981968000"
# press the back button to close the dialog and save the event
adb shell input keyevent 4
adb shell input keyevent 4

This only works, if the user already has a configured calendar. If not, it will fail with an error message in the GUI.

For the contacts, this is similar. We can call up the contacts with

adb shell "am start -a android.intent.action.INSERT -t vnd.android.cursor.dir/contact -e name 'Bo Lawson' -e phone 123456789"

But we can not affirmatively close the then opened save dialog, because keyevent 4 just cycles around with the "Discard changes" dialog: screen

zner0L commented 6 months ago

There is also the possibility to import contacts as vcards (https://stackoverflow.com/questions/23488290/android-importing-contacts-through-vcards-via-adb), however, this has a similar problem, because it requires interaction with the GUI. different versions of Android/different contacts apps act very differently here, so it is hard implement a general solution.

zner0L commented 6 months ago

Another option for adding contacts without having to interact with the GUI might be via the database. According to this article: https://www.dev2qa.com/android-contacts-database-structure/, contacts are saved in the /data/data/com.android.providers.contacts/databases/contacts2.db.

zner0L commented 6 months ago

Ok, with the help of this article I managed to write a frida skript that can be attached to com.android.contacts to add contacts without user interaction:

// This follows https://github.com/frida/frida/issues/1049
function loadMissingClass(className) {
    const loaders = Java.enumerateClassLoadersSync();
    let classFactory;
    for (const loader of loaders) {
        try {
            loader.findClass(className);
            classFactory = Java.ClassFactory.get(loader);
            break;
        } catch {
            // There was an error while finding the class, try another loader;
            continue;
        }
    }
    return classFactory.use(className);
}

function addContact(name) {
    const appContext = Java.use('android.app.ActivityThread').currentApplication().getApplicationContext();

    const ContentProviderOperation = Java.use('android.content.ContentProviderOperation');
    const ContactsContract = loadMissingClass('android.provider.ContactsContract');
    const RawContacts = loadMissingClass('android.provider.ContactsContract$RawContacts');
    const SyncColumns = loadMissingClass('android.provider.ContactsContract$SyncColumns');
    const Data = loadMissingClass('android.provider.ContactsContract$Data');
    const DataColumns = loadMissingClass('android.provider.ContactsContract$DataColumns');
    const StructuredName = loadMissingClass('android.provider.ContactsContract$CommonDataKinds$StructuredName');

    const ops = Java.use('java.util.ArrayList').$new();
    ops.add(
        ContentProviderOperation.newInsert(RawContacts.CONTENT_URI.value)
            .withValue(SyncColumns.ACCOUNT_TYPE.value, null)
            .withValue(SyncColumns.ACCOUNT_NAME.value, null)
            .build()
    );

    ops.add(
        ContentProviderOperation.newInsert(Data.CONTENT_URI.value)
            .withValueBackReference(DataColumns.RAW_CONTACT_ID.value, 0)
            .withValue(DataColumns.MIMETYPE.value, StructuredName.CONTENT_ITEM_TYPE.value)
            .withValue(StructuredName.DISPLAY_NAME.value, name)
            .build()
    );

    try {
        appContext.getContentResolver().applyBatch(ContactsContract.AUTHORITY.value, ops);
    } catch (e) {
        console.log(e);
    }
}
zner0L commented 5 days ago

It took some figuring out, but now I also have a script to create events on iOS. I do this while attached to the Calendar app, because it obviously already has the correct permissions. This is the script:

function addCalendarEvent(eventData) {
    const eventStore = ObjC.classes.EKEventStore.alloc().init();
    const NSError = ObjC.classes.NSError;
    const NSISO8601DateFormatter = ObjC.classes.NSISO8601DateFormatter;
    const NSString = ObjC.classes.NSString;
    const EKEvent = ObjC.classes.EKEvent;

    const formatter = NSISO8601DateFormatter.alloc().init();

    const evt = EKEvent.eventWithEventStore_(eventStore);
    evt.setTitle_(NSString.stringWithString_(eventData.title));
    const start = formatter.dateFromString_(NSString.stringWithString_(eventData.startDate));
    evt.setStartDate_(start);
    const end = formatter.dateFromString_(NSString.stringWithString_(eventData.endDate));
    evt.setEndDate_(end);
    evt.setCalendar_(eventStore.defaultCalendarForNewEvents());

    // https://github.com/frida/frida/issues/729
    const errorPtr = Memory.alloc(Process.pointerSize);
    Memory.writePointer(errorPtr, NULL);

    eventStore.saveEvent_span_commit_error_(evt, 0, 1, errorPtr);

    const error = Memory.readPointer(errorPtr);
    if (!error.isNull()) {
        const errorObj = new ObjC.Object(error); // now you can treat errorObj as an NSError instance
        console.error(errorObj.toString());
    }
}

It is based on Apple’s documentation and an example from an article.

zner0L commented 5 days ago

And here is a script for adding contacts (attached to the Contacts app) based on this stackoverflow answer:

function addContact(contactData) {
    const CNMutableContact = ObjC.classes.CNMutableContact;
    const NSString = ObjC.classes.NSString;
    const CNLabeledValue = ObjC.classes.CNLabeledValue;
    const CNPhoneNumber = ObjC.classes.CNPhoneNumber;
    const CNSaveRequest = ObjC.classes.CNSaveRequest;
    const NSMutableArray = ObjC.classes.NSMutableArray;
    const CNContactStore = ObjC.classes.CNContactStore;

    const contact = CNMutableContact.alloc().init();
    contact.setLastName_(NSString.stringWithString_(contactData.lastName));
    if(contactData.firstName) contact.setFirstName_(NSString.stringWithString_(contactData.firstName));

    if (contactData.phoneNumber) {
        const number = CNPhoneNumber.phoneNumberWithStringValue_(NSString.stringWithString_(contactData.phoneNumber));
        const homePhone = CNLabeledValue.labeledValueWithLabel_value_(NSString.stringWithString_('home'), number);
        const numbers = NSMutableArray.alloc().init();
        numbers.addObject_(homePhone);
        contact.setPhoneNumbers_(numbers);
    }

    if(contactData.email) {
         const email = NSString.stringWithString_(contactData.email);
         const homeEmail = CNLabeledValue.labeledValueWithLabel_value_(NSString.stringWithString_('home'), email);
         const emails = NSMutableArray.alloc().init();
         emails.addObject_(homeEmail);
         contact.setEmailAddresses_(emails);
    }

    const request = CNSaveRequest.alloc().init();
    request.addContact_toContainerWithIdentifier_(contact, null);

    // https://github.com/frida/frida/issues/729
    const errorPtr = Memory.alloc(Process.pointerSize);
    Memory.writePointer(errorPtr, NULL);

    const store = CNContactStore.alloc().init();
    store.executeSaveRequest_error_(request, errorPtr);

    const error = Memory.readPointer(errorPtr);
    if (!error.isNull()) {
        var errorObj = new ObjC.Object(error); // now you can treat errorObj as an NSError instance
        console.error(errorObj.toString());
    }
}
zner0L commented 23 hours ago

I implemented setting the device name here 42c5f6a (#134).

zner0L commented 23 hours ago

We put Geolocation on hold, while we decide on whether we want to use Appium, because it already implements this (see #21). Apple specific data and calls and messages are out of scope for now.