利用FRIDA攻擊Android應用程序(二)
在本係列文章的第一篇中,我們已經對Frida的原理進行了詳細的介紹,現在,我們將演示如何通過Frida搞定crackme問題。有了第一篇的內容作為基礎,理論上講這應該不是什麼難事。如果你想親自動手完成本文介紹的實驗的話,請下載
OWASP Uncrackable Crackme Level 1 (APK)
當然,這裏假定您已在計算機上成功地安裝了Frida(版本9.1.16或更高版本),並在(已經獲得root權限的)設備上啟動了相應服務器的二進製代碼。我們這裏將在模擬器中使用Android 7.1.1 ARM映像。
然後,請在您的設備上安裝Uncrackable Crackme Level 1應用程序:
adb install sg.vantagepoint.uncrackable1.apk
安裝完成後,從模擬器的菜單(右下角的橙色圖標)啟動它:
一旦啟動應用程序,您就會注意到它不太樂意在已經獲取root權限的設備上運行:
如果單擊“OK”,應用程序會立即退出。嗯,不太友好啊。看起來我們無法通過這種方法來搞定crackme。真是這樣嗎?讓我們看看到底怎麼回事,同時考察一下這個應用程序的內部運行機製。
現在,使用dex2jar將apk轉換為jar文件:
michael@sixtyseven:/opt/dex2jar/dex2jar-2.0$ ./d2j-dex2jar.sh -o /home/michael/UnCrackable-Level1.jar /home/michael/UnCrackable-Level1.apk
dex2jar /home/michael/UnCrackable-Level1.apk -> /home/michael/UnCrackable-Level1.jar
然後,將其加載到BytecodeViewer(或其他支持Java的反匯編器)中。你也可以嚐試直接加載到BytecodeViewer中,或直接提取classes.dex,但是試了一下好像此路不通,所以我才提前使用dex2jar完成相應的轉換。
為了使用CFR解碼器,需要在BytecodeViewer中依次選擇View-> Pane1-> CFR-> Java。如果你想將反編譯器的結果與Smali反匯編(通常比反編譯稍微準確一些)進行比較的話,可以將Pane2設置為Smali代碼。
下麵是CFR解碼器針對應用程序的MainActivity的輸出結果:
package sg.vantagepoint.uncrackable1;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.text.Editable;
import android.view.View;
import android.widget.EditText;
import sg.vantagepoint.uncrackable1.a;
import sg.vantagepoint.uncrackable1.b;
import sg.vantagepoint.uncrackable1.c;
public class MainActivity
extends Activity {
private void a(String string) {
AlertDialog alertDialog = new AlertDialog.Builder((Context)this).create();
alertDialog.setTitle((CharSequence)string);
alertDialog.setMessage((CharSequence)"This in unacceptable. The app is now going to exit.");
alertDialog.setButton(-3, (CharSequence)"OK", (DialogInterface.OnClickListener)new b(this));
alertDialog.show();
}
protected void onCreate(Bundle bundle) {
if (sg.vantagepoint.a.c.a() || sg.vantagepoint.a.c.b() || sg.vantagepoint.a.c.c()) {
this.a("Root detected!"); //This is the message we are looking for
}
if (sg.vantagepoint.a.b.a((Context)this.getApplicationContext())) {
this.a("App is debuggable!");
}
super.onCreate(bundle);
this.setContentView(2130903040);
}
public void verify(View object) {
object = ((EditText)this.findViewById(2131230720)).getText().toString();
AlertDialog alertDialog = new AlertDialog.Builder((Context)this).create();
if (a.a((String)object)) {
alertDialog.setTitle((CharSequence)"Success!");
alertDialog.setMessage((CharSequence)"This is the correct secret.");
} else {
alertDialog.setTitle((CharSequence)"Nope...");
alertDialog.setMessage((CharSequence)"That's not it. Try again.");
}
alertDialog.setButton(-3, (CharSequence)"OK", (DialogInterface.OnClickListener)new c(this));
alertDialog.show();
}
}
if (sg.vantagepoint.a.c.a() || sg.vantagepoint.a.c.b() || sg.vantagepoint.a.c.c())通過查看其他反編譯的類文件,我們發現它是一個小應用程序,並且貌似可以通過逆向解密例程和字符串修改例程來解決這個crackme問題。然而,既然有神器Frida在手,自然會有更方便的手段可供我們選擇。首先,讓我們看看這個應用程序是在哪裏檢查設備是否已獲取root權限的。在“Root detected”消息上麵,我們可以看到:
if (sg.vantagepoint.a.c.a() || sg.vantagepoint.a.c.b() || sg.vantagepoint.a.c.c())
如果你查看sg.vantagepoint.a.c類的話,你就會發現與root權限有關的各種檢查:
public static boolean a()
{
String[] a = System.getenv("PATH").split(":");
int i = a.length;
int i0 = 0;
while(true)
{
boolean b = false;
if (i0 >= i)
{
b = false;
}
else
{
if (!new java.io.File(a[i0], "su").exists())
{
i0 = i0 + 1;
continue;
}
b = true;
}
return b;
}
}
public static boolean b()
{
String s = android.os.Build.TAGS;
if (s != null && s.contains((CharSequence)(Object)"test-keys"))
{
return true;
}
return false;
}
public static boolean c()
{
String[] a = new String[7];
a[0] = "/system/app/Superuser.apk";
a[1] = "/system/xbin/daemonsu";
a[2] = "/system/etc/init.d/99SuperSUDaemon";
a[3] = "/system/bin/.ext/.su";
a[4] = "/system/etc/.has_su_daemon";
a[5] = "/system/etc/.installed_su_daemon";
a[6] = "/dev/com.koushikdutta.superuser.daemon/";
int i = a.length;
int i0 = 0;
while(i0 < i)
{
if (new java.io.File(a[i0]).exists())
{
return true;
}
i0 = i0 + 1;
}
return false;
}
在Frida的幫助下,我們可以通過覆蓋它們使所有這些方法全部返回false,這一點我們已經在第一篇中介紹過了。但是,當一個函數由於檢測到設備已經取得了root權限而返回true時,結果會怎樣呢? 正如我們在MainActivity函數中看到的那樣,它會打開一個對話框。此外,它還會設置一個onClickListener,當我們按下OK按鈕時就會觸發它:
alertDialog.setButton(-3, (CharSequence)"OK", (DialogInterface.OnClickListener)new b(this));
這個onClickListener的實現代碼如下所示:
package sg.vantagepoint.uncrackable1;
class b implements android.content.DialogInterface$OnClickListener {
final sg.vantagepoint.uncrackable1.MainActivity a;
b(sg.vantagepoint.uncrackable1.MainActivity a0)
{
this.a = a0;
super();
}
public void onClick(android.content.DialogInterface a0, int i)
{
System.exit(0);
}
}
它的功能並不複雜,實際上隻是通過System.exit(0)退出應用程序而已。所以我們要做的事情就是防止應用程序退出。為此,我們可以用Frida覆蓋onClick方法。下麵,讓我們創建一個文件uncrackable1.js,並把我們的代碼放入其中:
setImmediate(function() { //prevent timeout
console.log("[*] Starting script");
Java.perform(function() {
bClass = Java.use("sg.vantagepoint.uncrackable1.b");
bClass.onClick.implementation = function(v) {
console.log("[*] onClick called");
}
console.log("[*] onClick handler modified")
})
})
如果你已經閱讀了本係列文章的第一篇的話,這個腳本應該不難理解:將我們的代碼封裝到setImmediate函數中,以防止超時,然後通過Java.perform來使用Frida用於處理Java的方法。接下來,我們將得到一個類的包裝器,可用於實現OnClickListener接口並覆蓋其onClick方法。在我們的版本中,這個函數隻是向控製台寫一些輸出。與之前不同的是,它不會退出應用程序。由於原來的onClickHandler被替換為Frida注入的函數,因此它絕對不會被調用了,所以當我們點擊對話框的OK按鈕時,應用程序就不退出了。好了,讓我們實驗一下:打開應用程序(使其顯示“Root detected”對話框)
並注入腳本:
frida -U -l uncrackable1.js sg.vantagepoint.uncrackable1
Frida注入代碼需要幾秒鍾的時間,當你看到“onClick handler modified”消息時說明注入完成了(當然,注入完成時你也可以得到一個shell之前,因為可以把我們的代碼放入一個setImmediate包裝器中,從而讓Frida在後台執行它)。
然後,點擊應用程序中的OK按鈕。如果一切順利的話,應用程序就不會退出了。
我們看到對話框消失了,這樣我們就可以輸入密碼了。下麵讓我們輸入一些內容,點擊Verify,看看會發生什麼情況:
不出所料,這是一個錯誤的密碼。但是這並不要緊,因為我們真正要找的是:加密/解密例程以及結果和輸入的比對。
再次檢查MainActivity時,我們注意到了下麵的函數
public void verify(View object) {
它調用了類sg.vantagepoint.uncrackable1.a的方法:
if (a.a((String)object)) {
下麵是sg.vantagepoint.uncrackable1.a類的反編譯結果:
package sg.vantagepoint.uncrackable1;
import android.util.Base64;
import android.util.Log;
/*
* Exception performing whole class analysis ignored.
*/
public class a {
public static boolean a(String string) {
byte[] arrby = Base64.decode((String)"5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=", (int)0);
byte[] arrby2 = new byte[]{};
try {
arrby2 = arrby = sg.vantagepoint.a.a.a((byte[])a.b((String)"8d127684cbc37c17616d806cf50473cc"), (byte[])arrby);
}
catch (Exception var2_2) {
Log.d((String)"CodeCheck", (String)("AES error:" + var2_2.getMessage()));
}
if (!string.equals(new String(arrby2))) return false;
return true;
}
public static byte[] b(String string) {
int n = string.length();
byte[] arrby = new byte[n / 2];
int n2 = 0;
while (n2 < n) {
arrby[n2 / 2] = (byte)((Character.digit(string.charAt(n2), 16) << 4) + Character.digit(string.charAt(n2 + 1), 16));
n2 += 2;
}
return arrby;
}
}
注意在a方法末尾的string.equals比較,以及在上麵的try代碼塊中字符串arrby2的創建。arrby2是函數sg.vantagepoint.a.a.a的返回值。string.equals會將我們的輸入與arrby2進行比較。所以,我們要追蹤sg.vantagepoint.a.a的返回值。
現在,我們可以著手對這些字符串操作函數和解密函數進行逆向工程,並處理原始加密字符串了,實際上它們也包含在上麵的代碼中。或者,我們還可以讓應用程序替我們完成字符串的處理和加密工作,而我們隻要鉤住sg.vantagepoint.a.a.a函數來捕獲其返回值就可以坐享其成了。返回值是我們的輸入將要與之比較的解密字符串(它以字節數組的形式返回)。具體可以參考下麵的腳本:
aaClass = Java.use("sg.vantagepoint.a.a");
aaClass.a.implementation = function(arg1, arg2) {
retval = this.a(arg1, arg2);
password = ''
for(i = 0; i < retval.length; i++) {
password += String.fromCharCode(retval[i]);
}
console.log("[*] Decrypted: " + password);
return retval;
}
console.log("[*] sg.vantagepoint.a.a.a modified");
其中,我們覆蓋了sg.vantagepoint.a.a.a函數,截獲其返回值並將其轉換為可讀字符串。這正是我們要找的解密字符串,所以我們將其打印到控製台。
將上述代碼放到一起,就組成了一個完整的腳本:
setImmediate(function() {
console.log("[*] Starting script");
Java.perform(function() {
bClass = Java.use("sg.vantagepoint.uncrackable1.b");
bClass.onClick.implementation = function(v) {
console.log("[*] onClick called.");
}
console.log("[*] onClick handler modified")
aaClass = Java.use("sg.vantagepoint.a.a");
aaClass.a.implementation = function(arg1, arg2) {
retval = this.a(arg1, arg2);
password = ''
for(i = 0; i < retval.length; i++) {
password += String.fromCharCode(retval[i]);
}
console.log("[*] Decrypted: " + password);
return retval;
}
console.log("[*] sg.vantagepoint.a.a.a modified");
});
});
現在,我們來運行這個腳本。然後,將其保存為uncrackable1.js,並執行下列命令(如果Frida沒有自動重新運行的話)
frida -U -l uncrackable1.js sg.vantagepoint.uncrackable1
耐心等待,直到您看到消息sg.vantagepoint.a.a發生變化,然後在Root detected對話框中單擊OK,在secret code中輸入一些字符,然後按Verify按鈕。哎,運氣好像不太好啊。
但是,請注意Frida的輸出:
michael@sixtyseven:~/Development/frida$ frida -U -l uncrackable1.js sg.vantagepoint.uncrackable1
____
/ _ | Frida 9.1.16 - A world-class dynamic instrumentation framework
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://www.frida.re/docs/home/
[*] Starting script
[USB::Android Emulator 5554::sg.vantagepoint.uncrackable1]-> [*] onClick handler modified
[*] sg.vantagepoint.a.a.a modified
[*] onClick called.
[*] Decrypted: I want to believe
太好了。我們實際上已經得到了解密的字符串:I want to believe。那麼,我們趕緊輸入這個字符串,看看是否正確:
本文到此結束,但願讀者閱讀本文後,能夠對學習Frida的動態二進製插樁功能有所幫助。
最後更新:2017-04-07 21:25:10