Оригинал: 11x256.github.io
Часть 1
Введение
В этом и следующих постах мы поговорим о Frida, инструмент для динамической бинарной инструментации. Я покажу вам некоторые примеры, которые покажут ее возможности. Мы будет работать с небольшими Android приложениеми, их исходный код доступен на github, так что начнем.
Еще одна вещь: первое на что стоит смотреть - это документация. В этой статье она не будет повторятся, вместо этого я приведу примеры, сделав доку более понятной
Подготовка
Вы можете проверить руководство Quick-Start из оффциальной документации, а также руководство по установке сервера Android. Они должно быть достаточно простыми.
Вам также потребуется соответствующее окружение для самостоятельной сборки проекта, либо же вы можете просто скачать APK.
Для этого туториала потребуется root доступ к андроид девайсу. Для этого можно использовать реальное устройство, но я буду использовать эмулятор (Android 6.0 x86).
А также будет использоваться python 2.7
Пример №1
Code: Java
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 для того, чтобы изменить результат. Для этого мы сделаем следующие шаги:
Шаг первый
- Запуск Frida сервера
- Установка APK
- Запуск APK и прикрепление к нему Frida
- Хук на вызов функции fun
- Изменение аргументов, как мы захотим
Получаем доступ к root оболочке на эмуляторе android устройства и запуск frida server
Заметка: убедитесь, что adb в вашей переменной окружения PATH
Шаг второйCode:PS C:\Users\11x256> adb shell root@generic_x86:/ # /data/local/tmp/frida-server &
Установка APK на устройстве
Шаг третийCode: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 биндинги.
Code: 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 можно получить следующим образом:
Шаг четвертый и пятыйCode: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
Code: Javaconsole.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_activi ty");
//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
Code: Javapackage 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;
}
}
Я добавил две новые функции:
Если мы запустим скрипт из предыдущего примера, то он молча упадет. Для получения сообщения ошибки потребуется обработка сообщения присланного нашим JS к Python коду, это можно сделать так:
- fun, которая принимает и возвращает тип String. Так что теперь у нас есть две одноименные функции, но с разными сигнатурами
- secret который нигде не вызывается
Code: 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 и регистрирует эту функцию.
Теперь запустите этот код еще раз и вы получите следующее сообщение:
Оно говорит, что существует более одного метода с именем fun и то что, нам следует использовать либо fun.overload('java.lang.Stirng') или fun.overload('int' , 'int').Code:{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')",...
Для обработки этой ситуации, которая так часто встречается в обфусцированном android коде, мы используем перегруженные методы следующим образом:
Code: Javamy_class.fun.overload("int" , "int").implementation = function(x,y){ //hooking the old function
....
my_class.fun.overload("java.lang.String").implemen tation = function(x){ //hooking the new function
Первая строка хукает функцию fun, с двумя целочисленными параметрами, вторая - функцию с параметром строкой.
Теперь посмотрим как мы можем поменять строковый аргумент. Тип String - не примитивный тип, что означает, что это класс с методами и аттрибутами.
Существует два способа создать String объект в Java:
Оба метода в этом случае эквиваленты, но в некоторых случаях возможно использовать только вариант с оператором new.
- String test = "this is test string";
- String test = new String("this is also a string test");
Code: JavaScript//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").implemen tation = 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), которая находит инстансы заданного класса, посмотрим на пример:
Code: JavaScript#Javascript code
Java.choose("com.example.a11x256.frida_test.my_act ivity" , {
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(){}
});
Вывод будет следующим:
Мы вызвали secret сразу как смогли, поэтому переменная total не была модифицирована.Code:Found instance: com.example.a11x256.frida_test.my_activity@9600a96 Result of secret func: @@@###@@@
Для этого примера все. В следующем мы проконтролируем, где именно вызывать функцию secret.
Файлы: Download
Часть 3
Введение
В предыдущем примере мы смогли вызвать функцию secret при присоединении нашего JS скрипта в процесс целевого приложения. В этом туториале мы сможем вызвать secret множество раз с помощью Frida's RPC (удаленный вызов процедур).
Пример №3
Code: Javapackage 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.
Code: JavaScript//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_act ivity", {
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 приложения.
Code: Pythonimport 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 и напечатает ее результат.
Вывод:
Вместо того чтобы обходить кучу на каждый вызов в коде на JS, мы можем улучшить быстродействие путем сохранения объектов, найденных в куче. Поскольку my_activity создается лишь однажды, обновлять массив не требуется:Code: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
Code: JavaScript//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_act ivity", {
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
Code: Pythonpackage 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().toStr ing() + ":" + password_et.getText().toString()).getBytes(), Base64.DEFAULT));
}
});
}
}
Допустим у нас есть приложение с валидацией на стороне клиента на ввод "admin" для поля имени пользователя. Мы же, наоборот, хотим обойти эту защиту.
Существует множество способов это сделать, к примеру мы можем хукнуть compareTo функцию и вернуть ненулевое значение, но мы пойдем другим путем.
Вместо этого мы хукнем setText, отправим его аргументы в python, сделаем необходимые изменения, а затем отправим новые аргументы назад в андроид следующим образом:
Code: JavaScriptconsole.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 объект выглядит следующим образом:
Code: Pythonimport 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
Code: Javapackage 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:
Code: JavaScriptconsole.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").imple mentation = 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:
Code: Pythonimport 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()
Вывод
Пример вывода, полученный при коммуникации с локальным сервером:
Файлы: DowloadCode: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 !!!
© Translated by Kitsu special for r0 Crew



Reply With Quote
Thanks