R0 CREW

Перехват функций на Android с помощью Frida

Оригинал: 11x256.github.io

Часть 1

Введение

В этом и следующих постах мы поговорим о Frida, инструмент для динамической бинарной инструментации. Я покажу вам некоторые примеры, которые покажут ее возможности. Мы будет работать с небольшими Android приложениеми, их исходный код доступен на github, так что начнем.

Еще одна вещь: первое на что стоит смотреть - это документация. В этой статье она не будет повторятся, вместо этого я приведу примеры, сделав доку более понятной

Подготовка

Вы можете проверить руководство Quick-Start из оффциальной документации, а также руководство по установке сервера Android. Они должно быть достаточно простыми.

Вам также потребуется соответствующее окружение для самостоятельной сборки проекта, либо же вы можете просто скачать APK.

Для этого туториала потребуется root доступ к андроид девайсу. Для этого можно использовать реальное устройство, но я буду использовать эмулятор (Android 6.0 x86).

А также будет использоваться python 2.7

Пример №1

public class my_activity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_my_activity);
        while (true){

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            fun(50,30);
        }
    }

    void fun(int x , int y ){
        Log.d("Sum" , String.valueOf(x+y));
    }
}

Этот сниппет содержит часть кода для андроида. onCreate будет вызван при запуске приложения. Оно ожидает 1 секунду, после чего вызывается фунцию fun и повторяет бесконечно.

Функция fun будет печатать сумму двух аргументов (80), логи могут быть получены с помощью logcat.

Теперь мы будет использовать Frida для того, чтобы изменить результат. Для этого мы сделаем следующие шаги:

  1. Запуск Frida сервера
  2. Установка APK
  3. Запуск APK и прикрепление к нему Frida
  4. Хук на вызов функции fun
  5. Изменение аргументов, как мы захотим

Шаг первый

Получаем доступ к root оболочке на эмуляторе android устройства и запуск frida server

Заметка: убедитесь, что adb в вашей переменной окружения PATH

PS C:\Users\11x256> adb shell
root@generic_x86:/ # /data/local/tmp/frida-server &

Шаг второй

Установка APK на устройстве

PS C:\Users\11x256> adb install .\Desktop\app-1.apk
.\Desktop\app-1.apk: 1 file pushed. 49.0 MB/s (1573086 bytes in 0.031s)
        pkg: /data/local/tmp/app-1.apk
Success

Шаг третий

Frida инжектит Javascript в процессы, так что мы напишем JS код. Для автоматизации Frida мы будем использовать python биндинги.

#python code
import frida
import time
device = frida.get_usb_device()
pid = device.spawn(["com.example.a11x256.frida_test"])
device.resume(pid)
time.sleep(1) #Without it Java.perform silently fails
session = device.attach(pid)
script = session.create_script(open("s1.js").read())
script.load()

#prevent the python script from terminating
raw_input()

Этот код получит доступ к USB-устройству (в моем случае это эмулятор), запустит процесс, присоединится и продолжит его.

Имя пакета из APK можно получить следующим образом:

remnux@remnux:~/Desktop$ apktool d app-1.apk 
remnux@remnux:~/Desktop$ grep "package" ./app-1/AndroidManifest.xml 
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.a11x256.frida_test" platformBuildVersionCode="25" platformBuildVersionName="7.1.1">

Шаг четвертый и пятый

Теперь мы хотим написать JS код, который будет проинжектирован в запущенный процес для доступа и подмене аргументов нашей функции.

Мы уже знаем, что имя функции - fun, а класс который ее содержит - main_activity

console.log("Script loaded successfully ");
Java.perform(function x(){ //Silently fails without the sleep from the python code
    console.log("Inside java perform function");
    //get a wrapper for our class
    var my_class = Java.use("com.example.a11x256.frida_test.my_activity");
    //replace the original implmenetation of the function `fun` with our custom function
    my_class.fun.implementation = function(x,y){
    //print the original arguments
    console.log( "original call: fun("+ x + ", " + y + ")");
    //call the original implementation of `fun` with args (2,5)
    var ret_value = this.fun(2,5);
    return ret_value;
    }});

Результат

Теперь функция запущена с нашими аргументами (2, 5):

Вывод console.log содержится в нашей python консоли.

Файлы: Download

Часть 2

Введение

В предыдущем посте, я показал вам как можно перехватить вызовы функции, посмотреть и модифицировать аргументы. В этом посте мы попробуем тоже самое, но с другими типами аргументов, а также что делать с перегрузкой методов

Пример №2

package com.example.a11x256.frida_test;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;

public class my_activity extends AppCompatActivity {
    private String total = "@@@###@@@";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_my_activity);
        while (true){

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            fun(50,30);
            Log.d("string" , fun("LoWeRcAsE Me!!!!!!!!!"));
        }
    }

    void fun(int x , int y ){

        Log.d("Sum" , String.valueOf(x+y));
    }

    String fun(String x){
        total +=x;
        return x.toLowerCase();
    }

    String secret(){
        return total;
    }
}

Я добавил две новые функции:

  1. fun, которая принимает и возвращает тип String. Так что теперь у нас есть две одноименные функции, но с разными сигнатурами
  2. secret который нигде не вызывается

Если мы запустим скрипт из предыдущего примера, то он молча упадет. Для получения сообщения ошибки потребуется обработка сообщения присланного нашим JS к Python коду, это можно сделать так:

#python code
def my_message_handler(message , payload): #define our handler
print message
print payload
...
script.on("message" , my_message_handler) #register our handler to be called
script.load()

Мы определяем функцию, которая будет вызвана, когда JS код отправляет сообщение python и регистрирует эту функцию.

Теперь запустите этот код еще раз и вы получите следующее сообщение:

{u'columnNumber': 1, u'description': u"Error: fun(): has more than one overload, use .overload(<signature>) to choose from:\n\t.overload('java.lang.String')\n\t.overload('int', 'int')",...

Оно говорит, что существует более одного метода с именем fun и то что, нам следует использовать либо fun.overload(‘java.lang.Stirng’) или fun.overload(‘int’ , ‘int’).

Для обработки этой ситуации, которая так часто встречается в обфусцированном android коде, мы используем перегруженные методы следующим образом:

my_class.fun.overload("int" , "int").implementation = function(x,y){ //hooking the old function

....

my_class.fun.overload("java.lang.String").implementation = function(x){ //hooking the new function

Первая строка хукает функцию fun, с двумя целочисленными параметрами, вторая - функцию с параметром строкой.

Теперь посмотрим как мы можем поменять строковый аргумент. Тип String - не примитивный тип, что означает, что это класс с методами и аттрибутами.

Существует два способа создать String объект в Java:

  • String test = “this is test string”;
  • String test = new String(“this is also a string test”);

Оба метода в этом случае эквиваленты, но в некоторых случаях возможно использовать только вариант с оператором new.

//javascript code
var string_class = Java.use("java.lang.String"); // get a JS wrapper for java's String class

my_class.fun.overload("java.lang.String").implementation = function(x){ //hooking the new function
  console.log("*************************************");
  var my_string = string_class.$new("My TeSt String#####"); //creating a new String by using `new` operator 
  console.log("Original arg: " +x );
  var ret =  this.fun(my_string); // calling the original function with the new String, and putting its return value in ret variable
  console.log("Return value: "+ret);
  console.log("*************************************");
  return ret;
};

Теперь предположим, что мы хотим вызвать функцию secret. Она не вызывается из функции onCreate, поэтому хук на вызовы будет бесполезным.

Но мы ведь вызвали оператор new класса String, так что мы можем использовать тот же самый метод для вызова secret, верно? Не совсем так: в случае оператора new, мы создали новый объект класса String, но теперь мы не хотим создавать новый инстантс нашего класса my_activity. Мы хотим найти инстанс, который уже лежит в памяти и вызвать его функцию.

В Frida есть функция Java.choose(className, callbacks), которая находит инстансы заданного класса, посмотрим на пример:

#Javascript code
Java.choose("com.example.a11x256.frida_test.my_activity" , {
  onMatch : function(instance){ //This function will be called for every instance found by frida
    console.log("Found instance: "+instance);
    console.log("Result of secret func: " + instance.secret());
  },
  onComplete:function(){}

});

Вывод будет следующим:

Found instance: com.example.a11x256.frida_test.my_activity@9600a96
Result of secret func: @@@###@@@

Мы вызвали secret сразу как смогли, поэтому переменная total не была модифицирована.

Для этого примера все. В следующем мы проконтролируем, где именно вызывать функцию secret.

Файлы: Download

Часть 3

Введение

В предыдущем примере мы смогли вызвать функцию secret при присоединении нашего JS скрипта в процесс целевого приложения. В этом туториале мы сможем вызвать secret множество раз с помощью Frida’s RPC (удаленный вызов процедур).

Пример №3

package com.example.a11x256.frida_test;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;

public class my_activity extends AppCompatActivity {
    private String total = "@@@###@@@";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_my_activity);
        while (true){

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            fun(50,30);
            Log.d("string" , fun("LoWeRcAsE Me!!!!!!!!!"));
        }
    }

    void fun(int x , int y ){

        Log.d("Sum" , String.valueOf(x+y));
    }

    String fun(String x){
        total +=x;
        return x.toLowerCase();
    }

    String secret(){
        return total;
    }
}

Это тот же самый пример под Android что и ранее, различия будут в JS/Python.

//Javascript code
console.log("Script loaded successfully ");

function callSecretFun() { //Defining the function that will be exported
    Java.perform(function () { //code that calls `secret` function from the previous example

        Java.choose("com.example.a11x256.frida_test.my_activity", {
            onMatch: function (instance) {
                console.log("Found instance: " + instance);
                console.log("Result of secret func: " + instance.secret());
            },
            onComplete: function () { }

        });

    });


}
rpc.exports = {
    callsecretfunction: callSecretFun //exporting callSecretFun as callsecretfunction
  // the name of the export (callsecretfunction) cannot have  neither Uppercase letter nor uderscores.


};

Этот JS код определяет функцию callSecretFun, которую мы позже вызовем из питона для вызова secret из Android приложения.

import time
import frida

def my_message_handler(message, payload):
print message
print payload

device = frida.get_usb_device()
pid = device.spawn(["com.example.a11x256.frida_test"])
device.resume(pid)
time.sleep(1)  # Without it Java.perform silently fails
session = device.attach(pid)
with open("s3.js") as f:
script = session.create_script(f.read())
script.on("message", my_message_handler)
script.load()

command = ""
while 1 == 1:
command = raw_input("Enter command:\n1: Exit\n2: Call secret function\nchoice:")
if command == "1":
break
elif command == "2":
script.exports.callSecretFunction()

В коде на питоне добавлен бесконечный цикл для чтения ввода пользователя. Ввод “2” вызовет функцию callSecretFunction, которая в свою очередь вызовет функцию secret и напечатает ее результат.

Вывод:

Script loaded successfully 
Enter command:
1: Exit
2: Call secret function
choice:2
Found instance: com.example.a11x256.frida_test.my_activity@dfbf782
Result of secret func: @@@###@@@LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!
Enter command:
1: Exit
2: Call secret function
choice:2
Found instance: com.example.a11x256.frida_test.my_activity@dfbf782
Result of secret func: @@@###@@@LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!
Enter command:
1: Exit
2: Call secret function
choice:2
Found instance: com.example.a11x256.frida_test.my_activity@dfbf782
Result of secret func: @@@###@@@LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!
Enter command:
1: Exit
2: Call secret function
choice:
Process finished with exit code 1

Вместо того чтобы обходить кучу на каждый вызов в коде на JS, мы можем улучшить быстродействие путем сохранения объектов, найденных в куче. Поскольку my_activity создается лишь однажды, обновлять массив не требуется:

//javascript code
console.log("Script loaded successfully ");
var instances_array = [];
function callSecretFun() {
    Java.perform(function () {
        if (instances_array.length == 0) { // if array is empty
            Java.choose("com.example.a11x256.frida_test.my_activity", {
                onMatch: function (instance) {
                    console.log("Found instance: " + instance);
                    instances_array.push(instance)
                    console.log("Result of secret func: " + instance.secret());
                },
                onComplete: function () { }

            });
        }
        else {//else if the array has some values
            for (i = 0; i < instances_array.length; i++) {
                console.log("Result of secret func: " + instances_array[i].secret());
            }
        }

    });
}
rpc.exports = {
    callsecretfunction: callSecretFun
};

Файлы: Download

Часть 4

Введение

В этом посте мы не будем использовать console.log для вывода данных, вместо этого мы отправим данные из JS в Python для дополнительной обработки. После чего вернем результат обратно в JS, чтобы заинжектить его в память Android приложения.

Пример №4

package com.example.a11x256.frida_test;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Base64;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;

public class my_activity extends AppCompatActivity {
EditText username_et;
EditText password_et;
TextView message_tv;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my_activity);
password_et = (EditText) this.findViewById(R.id.editText2);
username_et = (EditText) this.findViewById(R.id.editText);
message_tv = ((TextView) findViewById(R.id.textView));
this.findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

if (username_et.getText().toString().compareTo("admin") == 0) {
message_tv.setText("You cannot login as admin");
return;
}
//hook target
message_tv.setText("Sending to the server :" + Base64.encodeToString((username_et.getText().toString() + ":" + password_et.getText().toString()).getBytes(), Base64.DEFAULT));

}
});
}
}

Допустим у нас есть приложение с валидацией на стороне клиента на ввод “admin” для поля имени пользователя. Мы же, наоборот, хотим обойти эту защиту.

Существует множество способов это сделать, к примеру мы можем хукнуть compareTo функцию и вернуть ненулевое значение, но мы пойдем другим путем.

Вместо этого мы хукнем setText, отправим его аргументы в python, сделаем необходимые изменения, а затем отправим новые аргументы назад в андроид следующим образом:

console.log("Script loaded successfully ");
Java.perform(function () {
    var tv_class = Java.use("android.widget.TextView");
    tv_class.setText.overload("java.lang.CharSequence").implementation = function (x) {
        var string_to_send = x.toString();
        var string_to_recv;
        send(string_to_send); // send data to python code
        recv(function (received_json_object) {
            string_to_recv = received_json_object.my_data
        }).wait(); //block execution till the message is received
        return this.setText(string_to_recv);
    }
});

Аргумент setText отправлен в питон, после этого функция recv будет ожидать JSON объект из питона.

Код на Python, который отправляет JSON объект выглядит следующим образом:

import time
import frida

def my_message_handler(message, payload):
print message
print payload
if message["type"] == "send":
# print message["payload"]
data = message["payload"].split(":")[1].strip()
# print 'message:', message
data = data.decode("base64")
user, pw = data.split(":")
data = ("admin" + ":" + pw).encode("base64")
# print "encoded data:", data
script.post({"my_data": data}) #send JSON object
print "Modified data sent"

device = frida.get_usb_device()
pid = device.spawn(["com.example.a11x256.frida_test"])
device.resume(pid)
time.sleep(1)  
session = device.attach(pid)
with open("s4.js") as f:
script = session.create_script(f.read())
script.on("message", my_message_handler) #register the message handler 
script.load()
raw_input()

Файлы: Download

Часть 5

Введение

В этом посте мы хукнем критографическую библиотеку Java с помощью Frida для того, чтобы получить данные в незашифрованном виде, а также криптографические ключи из андроид приложения.

Пример №5

package com.example.a11x256.frida_test;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Base64;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class my_activity extends AppCompatActivity {
    EditText username_et;
    EditText password_et;
    TextView message_tv;
    HttpURLConnection conn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_my_activity);
        message_tv = ((TextView) findViewById(R.id.textView));
        username_et = (EditText) findViewById(R.id.editText);
        password_et = (EditText) findViewById(R.id.editText2);
        ((Button) findViewById(R.id.button)).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                send_data(username_et.getText() + ":" + password_et.getText());
            }
        });

    }

    void send_data(final String data) {
        URL url = null;
        try {
            url = new URL("http://192.168.18.134");
            final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("POST");
            conn.setDoOutput(true);
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        DataOutputStream out = new DataOutputStream(conn.getOutputStream());
                        out.writeBytes(enc(data));
                        BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
                        final String text = in.readLine();
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                ((TextView) findViewById(R.id.textView)).setText(text);
                                dec(text);
                            }
                        });
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();

        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    String enc(String data) {
        try {
            String pre_shared_key = "aaaaaaaaaaaaaaaa"; //assume that this key was not hardcoded
            String generated_iv = "bbbbbbbbbbbbbbbb";
            Cipher my_cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            my_cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(pre_shared_key.getBytes("UTF-8"), "AES"), new IvParameterSpec(generated_iv.getBytes("UTF-8")));
            byte[] x = my_cipher.doFinal(data.getBytes());

            System.out.println(new String(Base64.encode(x, Base64.DEFAULT)));
            return new String(Base64.encode(x, Base64.DEFAULT));
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (InvalidAlgorithmParameterException e) {
            e.printStackTrace();
        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
        } catch (BadPaddingException e) {
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (IllegalBlockSizeException e) {
            e.printStackTrace();
        }
        return null;
    }

    String dec(String data) {
        try {
            byte[] decoded_data = Base64.decode(data.getBytes(), Base64.DEFAULT);
            String pre_shared_key = "aaaaaaaaaaaaaaaa"; //assume that this key was not hardcoded
            String generated_iv = "bbbbbbbbbbbbbbbb";
            Cipher my_cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            my_cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(pre_shared_key.getBytes("UTF-8"), "AES"), new IvParameterSpec(generated_iv.getBytes("UTF-8")));
            String plain = new String(my_cipher.doFinal(decoded_data));
            return plain;
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
        } catch (InvalidAlgorithmParameterException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (BadPaddingException e) {
            e.printStackTrace();
        } catch (IllegalBlockSizeException e) {
            e.printStackTrace();
        }
        return "";
    }
}

Это приложение использует шифр AES в режиме CBC для (де-)шифровки данных. Зашифрованные данные отправляются на HTTP сервер с помощью POST запроса. Данные принятые от сервера дешифруются и нигде не выводятся.

Ключи зашиты в приложение, в реальных приложениях они не будут. Их следует пересылать безопасно по сети во время исполнения.

Наша цель - это получение критографических ключей, пока они используются (после пересылки от сервера в реальных приложениях).

Код на JS:

console.log("Script loaded successfully 55");

Java.perform(function x() {
    var secret_key_spec = Java.use("javax.crypto.spec.SecretKeySpec");
    //SecretKeySpec is inistantiated with the bytes of the key, so we hook the constructor and get the bytes of the key from it
    //We will get the key but we won't know what data is decrypted/encrypted with it
    secret_key_spec.$init.overload("[B", "java.lang.String").implementation = function (x, y) {
        send('{"my_type" : "KEY"}', new Uint8Array(x));
        //console.log(xx.join(" "))
        return this.$init(x, y);
    }
    //hooking IvParameterSpec's constructor to get the IV as we got the key above.
    var iv_parameter_spec = Java.use("javax.crypto.spec.IvParameterSpec");
    iv_parameter_spec.$init.overload("[B").implementation = function (x) {
        send('{"my_type" : "IV"}', new Uint8Array(x));
        return this.$init(x);
    }
    //now we will hook init function in class Cipher, we will be able to tie keys,IVs with Cipher objects
    var cipher = Java.use("javax.crypto.Cipher");
    cipher.init.overload("int", "java.security.Key", "java.security.spec.AlgorithmParameterSpec").implementation = function (x, y, z) {
        //console.log(z.getClass()); 
        if (x == 1) // 1 means Cipher.MODE_ENCRYPT
            send('{"my_type" : "hashcode_enc", "hashcode" :"' + this.hashCode().toString() + '" }');
        else // In this android app it is either 1 (Cipher.MODE_ENCRYPT) or 2 (Cipher.MODE_DECRYPT)
            send('{"my_type" : "hashcode_dec", "hashcode" :"' + this.hashCode().toString() + '" }');
        //We will have two lists in the python code, which keep track of the Cipher objects and their modes.


        //Also we can obtain the key,iv from the args passed to init call
        send('{"my_type" : "Key from call to cipher init"}', new Uint8Array(y.getEncoded()));
        //arg z is of type AlgorithmParameterSpec, we need to cast it to IvParameterSpec first to be able to call getIV function
        send('{"my_type" : "IV from call to cipher init"}', new Uint8Array(Java.cast(z, iv_parameter_spec).getIV()));
        //init must be called this way to work properly
        return cipher.init.overload("int", "java.security.Key", "java.security.spec.AlgorithmParameterSpec").call(this, x, y, z);

    }
    //now hooking the doFinal method to intercept the enc/dec process
    //the mode specified in the previous init call specifies whether this Cipher object will decrypt or encrypt, there is no functions like cipher.getopmode() that we can use to get the operation mode of the object (enc or dec)
    //so we will send the data before and after the call to the python code, where we will decide which one of them is cleartext data
    //if the object will encrypt, so the cleartext data is availabe in the argument before the call, else if the object will decrypt, we need to send the data returned from the doFinal call and discard the data sent before the call
    cipher.doFinal.overload("[B").implementation = function (x) {
        send('{"my_type" : "before_doFinal" , "hashcode" :"' + this.hashCode().toString() + '" }', new Uint8Array(x));
        var ret = cipher.doFinal.overload("[B").call(this, x);
        send('{"my_type" : "after_doFinal" , "hashcode" :"' + this.hashCode().toString() + '" }', new Uint8Array(ret));

        return ret;
    }
});

Код на Python:

import time
import frida
import json

enc_cipher_hashcodes = [] #cipher objects with Cipher.ENCRYPT_MODE will be stored here
dec_cipher_hashcodes = [] #cipher objects with Cipher.ENCRYPT_MODE will be stored here

def my_message_handler(message, payload):
#mainly printing the data sent from the js code, and managing the cipher objects according to their operation mode
if message["type"] == "send":
# print message["payload"]
my_json = json.loads(message["payload"])
if my_json["my_type"] == "KEY":
print "Key sent to SecretKeySpec()", payload.encode("hex")
elif my_json["my_type"] == "IV":
print "Iv sent to IvParameterSpec()", payload.encode("hex")
elif my_json["my_type"] == "hashcode_enc":
enc_cipher_hashcodes.append(my_json["hashcode"])
elif my_json["my_type"] == "hashcode_dec":
dec_cipher_hashcodes.append(my_json["hashcode"])
elif my_json["my_type"] == "Key from call to cipher init":
print "Key sent to cipher init()", payload.encode("hex")
elif my_json["my_type"] == "IV from call to cipher init":
print "Iv sent to cipher init()", payload.encode("hex")
elif my_json["my_type"] == "before_doFinal" and my_json["hashcode"] in enc_cipher_hashcodes:
#if the cipher object has Cipher.MODE_ENCRYPT as the operation mode, the data before doFinal will be printed
#and the data returned (ciphertext) will be ignored
print "Data to be encrypted :", payload
elif my_json["my_type"] == "after_doFinal" and my_json["hashcode"] in dec_cipher_hashcodes:
print "Decrypted data :", payload
else:
print message
print '*' * 16
print payload

device = frida.get_usb_device()
pid = device.spawn(["com.example.a11x256.frida_test"])
device.resume(pid)
time.sleep(1)  # Without it Java.perform silently fails
session = device.attach(pid)

with open("s5.js") as f:
script = session.create_script(f.read())
script.on("message", my_message_handler)  # register the message handler
script.load()

raw_input()

Вывод

Пример вывода, полученный при коммуникации с локальным сервером:

Iv sent to cipher init() 62626262626262626262626262626262
Data to be encrypted : 6557:hardcoded_secret_password
Key sent to SecretKeySpec() 61616161616161616161616161616161
Iv sent to IvParameterSpec() 62626262626262626262626262626262
Key sent to cipher init() 61616161616161616161616161616161
Iv sent to cipher init() 62626262626262626262626262626262
Decrypted data : Can you see this secret message too !!!
Key sent to SecretKeySpec() 61616161616161616161616161616161
Iv sent to IvParameterSpec() 62626262626262626262626262626262
Key sent to cipher init() 61616161616161616161616161616161
Iv sent to cipher init() 62626262626262626262626262626262
Decrypted data : Can you see this secret message too !!!

Файлы: Dowload

© Translated by Kitsu special for r0 Crew
1 Like