Frida和安卓逆向的第一次实践

Frida和安卓逆向的第一次实践

[!NOTE]

本文在创作时使用了生成式人工智能进行辅助

需要的工具:

  • 具有ROOT权限的Android环境(我使用了Windows Subsystem For Android的面具版
  • Frida
  • Jadx-GUI
  • 安卓调试桥(ADB)
  • (可选)Material Files(质感文件,可以方便地移动文件和设置权限)
  • (可选)APK安装程序 (方便地通过双击apk文件直接安装到WSA上)

现在默认你安装和配置好了上面的所有工具,接下来我们开始吧!

开始

我们以Frida-Labs的0x6和0x9为例来试用Frida。

0x6

安装Frida 0x6,启动后如图:

Frida 0x6的UI

什么都没有欸!那只能反编译了,嘿嘿。

将Frida 0x6的安装包拽进Jadx,获得以下页面:Jadx刚刚打开的截图

空的欸!代码都不显示,气死了!进不去,怎么想都进不去嘛!

这个时候,我们观察左栏,发现了“源代码”和“资源文件”。我们知道:

  • 源代码就是各种各样执行功能的代码啦!二进制选手最喜欢的东西!
  • APK signature是apk的签名,和软件打开长什么样没有什么关系,不管
  • 资源文件,什么图片啦,布局啦,字符串啦,把Flag放这里,没有意思吧?

所以当然是打开源代码啦!聪明的你一定知道,apk有很多不同的界面,它们是一个一个不同的活动(Activity | Android Developers (google.cn))。考虑到一打开app就这么一个页面,那代码肯定是写到最重要的Activity——MainActivity里面啦!

[!CAUTION]

虽然话这么说,但是这只是一般来说。大部分的App启动后的入口都是MainActivity,大概可以理解成main函数吧,不过也有例外,比如LSPosed,桌面,甚至是一些没有界面的玩意,比如字体、主题、插件什么什么的。不是每一个安卓应用程序的入口都是 MainActivity,也不是每个 APK 都包含 MainActivityMainActivity 只是一个常见的类名,很多开发者用它来表示应用程序的主活动(Activity),但这并不是强制的。
在安卓应用程序中,入口点通常是一个活动(Activity),它是应用程序中负责与用户交互的界面。这个活动不一定要命名为 MainActivity,它可以有任何名称。开发者可以根据应用程序的逻辑和结构来命名这个活动。
安卓系统的入口点是由 AndroidManifest.xml 文件中的 <intent-filter> 标签定义的,特别是带有 android.intent.action.MAINandroid.intent.category.LAUNCHER 的过滤器。这个过滤器指示了当用户点击应用程序图标时应该启动哪个活动。例如:

1
2
3
4
5
6
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

在上面的例子中,MainActivity 是应用程序的入口点,因为它包含了上述的 <intent-filter>。如果开发者想要使用不同的活动作为入口点,他们只需要在 AndroidManifest.xml 文件中相应地修改 <activity> 标签即可。
此外,有些应用程序可能根本不包含任何活动,例如一些后台服务或只在服务器上运行的程序。还有一些应用程序可能有多个活动,但只有一个被设置为启动活动,即用户点击应用程序图标时看到的第一个界面。
总之,安卓应用程序的入口点是由 AndroidManifest.xml 文件中定义的,而不是由活动类的名称决定的。开发者可以根据需要为活动命名,并指定哪个活动作为应用程序的入口点。

———By ChatGLM4

我们依次打开:

MainActivity的代码

很快就看到了打印Flag的函数。简单读一下,是从Get_Flag获取Num1 和 Num2 后,检查是不是和期望的值(1234、4321)相等,再解密一串密码,最后通过修改t1(也就是你看到的那个“Hello World”)来显示flag。需要注意的是,虽说里面出现了Base64,但是你直接拿去解密是解不开哒😘

接下来我们有三种方法来让Flag显示出来:

  • 老老实实硬看,搞懂加密逻辑(太笨了不会做,因此我放弃这个方法)
  • 爆改代码,重新打包一个apk
  • 用frida把 1234 和 4321 给它传回去

法1 爆改代码

在Jadx中,选择文件→另存为Gradle项目,再把得到的东西拿Android Studio打开,再小小地修改一下MainActivity里面的代码:

[!NOTE]

注意不要把文件存到含非ASCII的路径,不然Android Studio会摆烂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MainActivity extends AppCompatActivity {
TextView t1;

/* JADX INFO: Access modifiers changed from: protected */
@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
this.t1 = (TextView) findViewById(R.id.textview);
this.ChangeText();//在这里更新文本
}


public void ChangeText(){//删除了判断逻辑,直接执行解密代码
Cipher cipher = Cipher.getInstance("AES");
SecretKeySpec secretKeySpec = new SecretKeySpec("MySecureKey12345".getBytes(), "AES");
cipher.init(2, secretKeySpec);
byte[] decryptedBytes = Base64.getDecoder().decode("QQzMj/JNaTblEHnIzgJAQkvWJV2oK9G2/UmrCs85fog=");
String decrypted = new String(cipher.doFinal(decryptedBytes));
this.t1.setText(decrypted);
}
}

保存完我们编译一下:

又在下Gradle,气死了

算了我们还是不要用这个什么Android Studio了。换apktools。

1
java -jar apktool.jar d 'Challenge 0x6.apk' -o Challenge #解个包先

用Jadx在smali3里面找到了MainActivity的Smali。

image-20240330232426773

再用VS Code去编辑Smali,最后打包安装:image-20240331003336620

算了还是看法二吧,法一等我会了smali再说。

法二 拿Frida来解

首先先连接好

1
2
3
4
adb connect localhost:58526
adb shell
su
/data/local/frida-server-16.2.1 &

另开一个窗口

1
2
3
adb forward tcp:27042 tcp:27042 #注意,Frida server默认是27042端口
frida -U -f com.ad2001.frida0x6 #连接

如果出现

1
2
3
4
5
6
7
8
9
10
11
12
13
C:\Users\COTOMO>frida -U -f com.ad2001.frida0x6
____
/ _ | Frida 16.2.1 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
. . . .
. . . . Connected to Pixel 5 (id=localhost:58526)
Spawned `com.ad2001.frida0x6`. Resuming main thread!

那就是唱歌跳舞的时间

[!IMPORTANT]

失败了的话多看看报错内容,包括忘了端口转发,以及两个frida的版本不一致什么的。

接下来我们需要编写脚本。考虑到前面已经看过代码就写简单一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Java.performNow(function() {//立即执行,不用管
Java.choose('com.ad2001.frida0x6.MainActivity', {//选择类
onMatch: function(instance) {//onMatch是一个回调函数

var checker = Java.use("com.ad2001.frida0x6.Checker");//使用类
var checker_obj = checker.$new(); // 创建一个对象,目的是调用类中的函数
checker_obj.num1.value = 1234;
checker_obj.num2.value = 4321; //这两行代码设置 Checker 实例的 num1 和 num2 属性的值。value 属性是 Frida 用来获取和设置字段值的方法。
instance.get_flag(checker_obj); // 调用函数

},
onComplete: function() {}//没什么用但必须写上
});
});

[!TIP]

回调函数是一个作为参数传递给另一个函数的函数,它在某些操作完成后被调用以执行进一步的操作。

onMatch 是 Frida 中的一个回调函数,用于在 Java 进程的内存堆中搜索特定类型的实例时使用。当你使用 Java.choose 方法时,Frida 会遍历 Java 堆,查找所有匹配指定类型的实例。每当找到一个匹配的实例时,onMatch 回调函数就会被调用。

——By ChatGLM4

将上面的脚本直接扔到终端去执行,就能得到下面的结果:

image-20240330230233227

0x9

MainActivity代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package com.ad2001.a0x9;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.ad2001.a0x9.databinding.ActivityMainBinding;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;

/* loaded from: classes3.dex */
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
Button btn;

public native int check_flag();

static {
System.loadLibrary("a0x9");
}

/* JADX INFO: Access modifiers changed from: protected */
@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding inflate = ActivityMainBinding.inflate(getLayoutInflater());
this.binding = inflate;
setContentView(inflate.getRoot());
Button button = (Button) findViewById(R.id.button);
this.btn = button;
button.setOnClickListener(new View.OnClickListener() { // from class: com.ad2001.a0x9.MainActivity.1
@Override // android.view.View.OnClickListener
public void onClick(View v) {
if (MainActivity.this.check_flag() == 1337) {
try {
Cipher cipher = Cipher.getInstance("AES");
SecretKeySpec secretKeySpec = new SecretKeySpec("3000300030003003".getBytes(), "AES");
try {
cipher.init(2, secretKeySpec);
byte[] decryptedBytes = Base64.getDecoder().decode("hBCKKAqgxVhJMVTQS8JADelBUPUPyDiyO9dLSS3zho0=");
try {
String decrypted = new String(cipher.doFinal(decryptedBytes));
Toast.makeText(MainActivity.this.getApplicationContext(), "You won " + decrypted, 1).show();
return;
} catch (BadPaddingException e) {
throw new RuntimeException(e);
} catch (IllegalBlockSizeException e2) {
throw new RuntimeException(e2);
}
} catch (InvalidKeyException e3) {
throw new RuntimeException(e3);
}
} catch (NoSuchAlgorithmException e4) {
throw new RuntimeException(e4);
} catch (NoSuchPaddingException e5) {
throw new RuntimeException(e5);
}
}
Toast.makeText(MainActivity.this.getApplicationContext(), "Try again", 1).show();
}
});
}
}

这说明程序将在按下按钮的时候检查check_flag的返回值,如果是1337那么就显示flag。而check_flag本质上来说会调用a0x9动态链接库。所有我们把这个动态链接库拿出来看:(直接把apk文件解压,lib文件夹里面的就是)选择amd64架构的方便分析,IDA启动!

1
2
3
4
__int64 Java_com_ad2001_a0x9_MainActivity_check_1flag()
{
return 1LL;
}

?您就返回一个数就完了?

[!TIP]

作者还试图修改了动态链接库的内容,将动态链接库的返回值从1改成了1337,可是在安装的时候报错了,聪明的你知道是为什么吗?

打开计算器,发现1337=0x539

image-20240331015234952

回到VS Code,在”\Challenge0x9\smali_classes3\com\ad2001\a0x9\MainActivity.smali”里面发现了MainActivity$1的代码,打开搜索定位到0x539,修改成0x1,保存,接下来执行命令:

1
2
java -jar .\apktool.jar b .\Challenge0x9 patched.apk
.\zipalign.exe -f -v 4 ".\Challenge 0x9\dist\Challenge 0x9.apk" cc.apk

最后对cc.apk签名,安装,打开运行:

?不行?哪里出问题了?没反应啊?20240331_021458

30分钟过后,作者在不断debug的时候,突发奇想地把这个apk装在了自己的手机上时发现,答案是在Toast里面显示的,以及,Screenshot_20240331_023641_frida 0x9

屏幕截图 2024-03-31 023210

微软你作恶多端为什么不把Toast好好显示非要整到通知栏里面!!!!!害我半天找不到(气急败坏)

以下是Frida篇:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 获取 check_flag 函数的地址
var check_flag = Module.findExportByName("liba0x9.so", "Java_com_ad2001_a0x9_MainActivity_check_1flag");

// Hook check_flag 函数
Interceptor.attach(check_flag, {
onEnter: function (args) {
console.log('Entering ' + check_flag);
// 打印发生了什么
},
onLeave: function (retval) {
console.log('Leaving ' + check_flag);
// 打印原始返回值
console.log("Original return value: " + retval);
// 修改返回值
retval.replace(0x539);//1337的16进制表式
}
});

运行也能得到flag

1
FRIDA{NATIVE_LAND_0x2}

就到这里结束!