Skip to content
LaurieWired edited this page Aug 16, 2023 · 15 revisions

ARTful Overview

ARTful is an open-source tool that allows users to manipulate the Android runtime. It works by hooking two specified Android methods "targetArtMethod" and "newArtMethod". ARTful then overwrites the associated method data of "targetArtMethod" with the contents of "newArtMethod" causing the new method to unexpectedly execute instead each time the target is invoked. This tool was written and tested on Android13, but will also work on multiple other versions.

Requirements and Capabilities

The following is a list of important notes for using ARTful and understanding suitable target methods:

  • Method signatures must match
  • Methods must be static
  • Replacement of methods occurs only within your own application
  • Target methods can be public or private
  • No root required!

Using the Example ARTful Application

The "Source" folder of the ARTful repository contains an example Android application. Simply import this project into Android Studio and begin manipulating the Android Runtime! The ARTful library source is written in native C++ and can be found at native-lib.cpp. Example use and invocations of the library from Java are inside MainActivity.java. The default behavior is to swap two methods within the same class, but native declarations of all ARTful native methods are declared inside the MainActivity. See below for details on implementing each method.

Using ARTful as a Library

Importing the Library by Importing the AAR (Option 1)

Add the ARTful.aar file to your app\libs folder in your Android project:

aar_pic

Then add ARTful inside the "dependencies" section of your build.gradle.

implementation files('libs/ARTful.aar')

Importing the Library by Adding the SO Binaries (Option 2)

If you already followed the steps for importing in the previous option, skip this section. It is much easier to import the AAR file described above, but you could also import the shared object binaries directly using the below steps.

Add the libartful.so binaries to your Android project:

  1. Create the jniLibs directory inside your project at app\src\main
  2. Select your target instruction set architectures
  3. Add the libartful.so binaries in their architecture folders

The native libartful.so binaries can be found within this repository inside the Library folder. The structure of your app should then look like this:

project/
├──src/
   └── main/
       ├── AndroidManifest.xml
       ├── java/
       └── jniLibs/ 
           ├── arm64-v8a/                       <-- ARM 64-bit
           │   └── libartful.so
           ├── armeabi-v7a/                     <-- ARM 32-bit
           │   └── libartful.so
           ├── x86_64                           <-- Intel 64-bit
           │   └── libartful.so
           └── x86/                             <-- Intel 32-bit
               └── libartful.so

Next, add the "jniLibs" folder to your build.gradle:

sourceSets {
    main {
        jniLibs.srcDirs = ['src/main/jniLibs']
    }
}

Then create your own ArtfulLibraryHelper.java class.

Calling the Imported Library

Once you have imported the library into the project, you can now load and call it from your desired Java class:

System.loadLibrary("artful");
ArtfulLibraryHelper.registerNativeMethodsForClass(MainActivity.class) // Replace MainActivity with desired class

Finally, declare any native methods that you would like to use in your Java application. The full list of native method options can be found below.

public native void replaceAppMethodByObject(Object targetObject, Object newObject);
public native void replaceAppMethodBySignature(String targetClassName, String targetMethodName, String newClassName, String newMethodName, String methodSignature);
public native void printArtMethodOffsets();
public native void replaceGetRadioVersionByObject(Object newObject);
public native void replaceGetRadioVersionBySignature(String newClassName, String newMethodName);
public native void replaceLogEByObject(Object newObject);
public native void replaceLogEBySignature(String newClassName, String newMethodName);
public native void replaceToastMakeTextByObject(Object newObject);
public native void replaceToastMakeTextBySignature(String newClassName, String newMethodName);
public native void replacePatternMatchesByObject(Object newObject);
public native void replacePatternMatchesBySignature(String newClassName, String newMethodName);

ARTful Method Reference

The following contains a full list of how to use all ARTful methods once you have imported and declared the library inside of your project.

Replacing Via Method Signature

The method signature is used to encode the parameters and return value of a method to uniquely identify it. Simply call the replaceAppMethodBySignature() method from ARTful to select methods to be replaced based on signature. To find a complete reference of how to write method signatures, check out the JNI Signature Reference.

Example:

replaceAppMethodBySignature("com/app/artful/MainActivity", "benignMethod", "com/app/artful/MainActivity", "newMethod", "()Ljava/lang/String;");

Replacing Via Object

Passing objects is easier syntactically, but must also catch and handle runtime exceptions. The following code demonstrates how to select the target and replacement methods via object.

Example:

try {
    replaceAppMethodByObject(MainActivity.class.getDeclaredMethod("benignMethod"), MainActivity.class.getDeclaredMethod("newMethod"));
} catch (NoSuchMethodException e) {
    throw new RuntimeException(e);
}

Replacing Android Framework Methods

You can replace any static method located inside the Android Framework with your own method inside of your app. A list of multiple Android APIs that ARTful can automatically replace can be found below. If you would like to replace one not already added to ARTful, you could instead call the replaceAppMethodBySignature or replaceAppMethodByObject methods and simply pass in the references to the Android Framework method. Note that depending on the method you choose, you may experience strange behavior in your application if another method is dependent upon the normal behavior. Try to select static library methods that are not used elsewhere since you will be completely overwriting them for your application.

Creating a Replacement Method

If you want to replace any method in the Android Framework, simply write a new static method in your Java code with a matching signature. Then you can call the replace method and your new code will execute. For example, the declaration of the Build.getRadioVersion() method is defined as follows:

public static String getRadioVersion();

This means that we could replace this with our own method with a matching signature like the following:

public static String newMethod() {
    Log.d("ARTful", "I should not execute >:)");
    return "lol";
}

Replacing Log.e(tag, msg)

Implement a replacement method with a matching signature:

public static int newMethod(String tag, String msg) {
    Log.d("ARTful", "I should not execute >:)");

    // Your code goes here!


    return 1;
}

From Java code, declare and call the replace method from the ARTful library:

// Declare methods in your class
public native void replaceLogEByObject(Object newObject);
public native void replaceLogEBySignature(String newClassName, String newMethodName);

// Call by object or signature
try {
    replaceLogEByObject(MainActivity.class.getDeclaredMethod("newMethod", String.class, String.class));
} catch (NoSuchMethodException e) {
    throw new RuntimeException(e);
}

// New invocations below will actually trigger newMethod
Log.e("ARTful", "I'm benign");

Replacing Build.getRadioVersion()

Implement a replacement method with a matching signature:

public static String newMethod() {
    Log.d("ARTful", "I should not execute >:)");

    // Your code goes here!

    return "lol";
}

From Java code, declare and call the replace method from the ARTful library:

// Declare methods in your class
public native void replaceGetRadioVersionByObject(Object newObject);
public native void replaceGetRadioVersionBySignature(String newClassName, String newMethodName);

// Call by object or signature
try {
    replaceGetRadioVersionByObject(MainActivity.class.getDeclaredMethod("newMethod"));
} catch (NoSuchMethodException e) {
    throw new RuntimeException(e);
}

// New invocations below will actually trigger newMethod
Build.getRadioVersion();

Replacing Toast.makeText()

Implement a replacement method with a matching signature:

public static Toast newMethod(Context context, CharSequence text, int duration) {
    Log.d("ARTful", "I should not execute >:)");

    // Your code goes here!


    return new Toast(context);
}

From Java code, declare and call the replace method from the ARTful library:

// Declare methods in your class
public native void replaceToastMakeTextByObject(Object newObject);
public native void replaceToastMakeTextBySignature(String newClassName, String newMethodName);

// Call by object or signature
try {
    replaceToastMakeTextByObject(MainActivity.class.getDeclaredMethod("newMethod", Context.class, CharSequence.class, int.class));
} catch (NoSuchMethodException e) {
    throw new RuntimeException(e);
}

// New invocations below will actually trigger newMethod
Toast.makeText(getApplicationContext(), "This will not print", Toast.LENGTH_LONG);

Replacing Pattern.matches()

Implement a replacement method with a matching signature:

public static boolean newMethod(String regex, CharSequence input) {
    Log.d("ARTful", "I should not execute >:)");

    // Your code goes here!


    return true;
}

From Java code, declare and call the replace method from the ARTful library:

// Declare methods in your class
public native void replacePatternMatchesByObject(Object newObject);
public native void replacePatternMatchesBySignature(String newClassName, String newMethodName);

// Call by object or signature
try {
    replacePatternMatchesByObject(MainActivity.class.getDeclaredMethod("newMethod", String.class, CharSequence.class));
} catch (NoSuchMethodException e) {
    throw new RuntimeException(e);
}

// New invocations below will actually trigger newMethod
Pattern.matches("MyRegex", "Not actually tested");

Printing ArtMethod Offsets

Dummy classes are included to allow ARTful to print the member variable offsets of the ArtMethod class in Android13. These offsets can change between versions of Android. They are defined inside of art_method.h. Print offsets from Java code by calling:

printArtMethodOffsets();

logcat_offsets

If there are difference in variables between Android versions, you can update the dummy code to include the variables from your target Android version and print the correct offsets.