Frida和安卓逆向的第一次实践
[!NOTE]
本文在创作时使用了生成式人工智能进行辅助
需要的工具:
现在默认你安装和配置好了上面的所有工具,接下来我们开始吧!
开始
我们以Frida-Labs的0x6和0x9为例来试用Frida。
0x6
安装Frida 0x6,启动后如图:

什么都没有欸!那只能反编译了,嘿嘿。
将Frida 0x6的安装包拽进Jadx,获得以下页面:
空的欸!代码都不显示,气死了!进不去,怎么想都进不去嘛!
这个时候,我们观察左栏,发现了“源代码”和“资源文件”。我们知道:
- 源代码就是各种各样执行功能的代码啦!二进制选手最喜欢的东西!
- APK signature是apk的签名,和软件打开长什么样没有什么关系,不管
- 资源文件,什么图片啦,布局啦,字符串啦,把Flag放这里,没有意思吧?
所以当然是打开源代码啦!聪明的你一定知道,apk有很多不同的界面,它们是一个一个不同的活动(Activity | Android Developers (google.cn))。考虑到一打开app就这么一个页面,那代码肯定是写到最重要的Activity——MainActivity里面啦!
[!CAUTION]
虽然话这么说,但是这只是一般来说。大部分的App启动后的入口都是MainActivity,大概可以理解成main函数吧,不过也有例外,比如LSPosed,桌面,甚至是一些没有界面的玩意,比如字体、主题、插件什么什么的。不是每一个安卓应用程序的入口都是 MainActivity,也不是每个 APK 都包含 MainActivity。MainActivity 只是一个常见的类名,很多开发者用它来表示应用程序的主活动(Activity),但这并不是强制的。
在安卓应用程序中,入口点通常是一个活动(Activity),它是应用程序中负责与用户交互的界面。这个活动不一定要命名为 MainActivity,它可以有任何名称。开发者可以根据应用程序的逻辑和结构来命名这个活动。
安卓系统的入口点是由 AndroidManifest.xml 文件中的 <intent-filter> 标签定义的,特别是带有 android.intent.action.MAIN 和 android.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
我们依次打开:

很快就看到了打印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;
@Override 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); } }
|
保存完我们编译一下:

算了我们还是不要用这个什么Android Studio了。换apktools。
1
| java -jar apktool.jar d 'Challenge 0x6.apk' -o Challenge #解个包先
|
用Jadx在smali3里面找到了MainActivity的Smali。

再用VS Code去编辑Smali,最后打包安装:
算了还是看法二吧,法一等我会了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) { var checker = Java.use("com.ad2001.frida0x6.Checker"); var checker_obj = checker.$new(); checker_obj.num1.value = 1234; checker_obj.num2.value = 4321; instance.get_flag(checker_obj); }, onComplete: function() {} }); });
|
[!TIP]
回调函数是一个作为参数传递给另一个函数的函数,它在某些操作完成后被调用以执行进一步的操作。
onMatch 是 Frida 中的一个回调函数,用于在 Java 进程的内存堆中搜索特定类型的实例时使用。当你使用 Java.choose 方法时,Frida 会遍历 Java 堆,查找所有匹配指定类型的实例。每当找到一个匹配的实例时,onMatch 回调函数就会被调用。
——By ChatGLM4
将上面的脚本直接扔到终端去执行,就能得到下面的结果:

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;
public class MainActivity extends AppCompatActivity { private ActivityMainBinding binding; Button btn;
public native int check_flag();
static { System.loadLibrary("a0x9"); }
@Override 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() { @Override 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

回到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签名,安装,打开运行:
?不行?哪里出问题了?没反应啊?
30分钟过后,作者在不断debug的时候,突发奇想地把这个apk装在了自己的手机上时发现,答案是在Toast里面显示的,以及,

微软你作恶多端为什么不把Toast好好显示非要整到通知栏里面!!!!!害我半天找不到(气急败坏)
以下是Frida篇:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| var check_flag = Module.findExportByName("liba0x9.so", "Java_com_ad2001_a0x9_MainActivity_check_1flag");
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); } });
|
运行也能得到flag
就到这里结束!