注
此篇文章适合纯新手进行学习。
前言
安卓逆向是一门非常有意思的学问,它对受试者的考察包括但不限于Java代码读写能力,漏洞审计能力,JS脚本编写能力,反调试绕过能力,Native层代码阅读修复能力等,相当综合,甚至有时非常有挑战性,但重要的是自己多去尝试做而不是只看理论和教学,安卓逆向对实践的需求非常大。
Java层
一般来说安卓逆向会分为Java层和Natiev层,分别是apk和so文件。其中较为重要的是Java层,因为Java层可以看到一整个app的运行逻辑,包括但不限于各种运算逻辑,Native层的调用,各种资源的引用,甚至是一些隐藏的算法逻辑都可以在Java层中找到,对做题的作用巨大。
我们对Java层的定义一般是apk丢进JADX中反编译之后所显现出的形式,如下图所示

阅读Java层时需要做到事无巨细,拿以下代码为例
1 | // e.AbstractActivityC0110h, androidx.activity.k, z.f, android.app.Activity |
在看Java层时我们主要看各种类的调用,但是在有混淆的情况下就不是很好看了,像上面这个代码,除了创建了按钮,还调用了一个名为h和一个名为c的类(分别在第5行和第9行)。知道了调用类之后我们就能追踪类来找到一些加密的真实逻辑,但由于代码量的巨大所以有时候这最关键的部分会变得很困难。
值得一提的是,当我们从Java层抽取数据到C语言代码中编写逆向脚本时,最好使用uint8_t来定义数组或者变量,这样就能避免由于编译器和底层运算逻辑的不同而导致的数据截断问题了。
JADX
做题自然少不了工具,而JADX就是我们在做安卓逆向时的好帮手,GitHub上能下,下载就不说了,我们说说基本使用,除了上图所显示的代码块JADX还有如下的显示

左侧跟IDA中的functions功能近似就不赘述了,主要讲讲上面的功能
长得像房子一样的是一步定位到main逻辑,长得像准心的东西是在侧边栏同步右侧代码块所在的层级。这两个功能表面看起来没什么但是笔者曾经是不知道的,所以在做一道找隐藏函数的题时手动翻函数非常累还没翻到,但是有了这两个功能就可以实现快速定位main逻辑来找附近的隐藏函数。然后就是看起来平平无奇的文件

其中的首选项用处极大。有时做题会遇到JADX出现类似于下图的反编译错误

这个时候进入首选项勾选下图所示的选项

就可以发现代码可读性大大增强

Native层
Native层可谓是安卓逆向中最恶心的部分之一,因为当我们把so文件丢进IDA中会出现包括但不限于反编译错误,结构体错误,极为的严重混淆等问题,加了逆向分析的难度和时间。但面对灾难我们并非无计可施(玩梗别介意,我也想让这篇文章多点趣味),限于笔者水平,Native层的破解之法一般有两个:一个是直接静态分析;一个是Frida直接hook调用——这种一般适用于自解密逻辑或者自逆逻辑,但由于笔者还没用过所以暂时不是很清楚。Native层逆向出得比较好的两道题目分别是NCTF的安卓逆向和星盟招新赛的安卓逆向。
Native层逆向其实跟普通的exe逆向区别不是很大(星盟招新赛那题是个例外),不赘述了,主要靠自己多体会。
例题
FridaLab-0x1
1 | void check(int i, int i2) { |
由于这里给了目标字符串和逻辑,我们直接抄就能抄过来,不过其实这就是个位移密码(凯撒),左移21位,可以在线工具一把梭

但是这样就少了很多趣味,并且学不到什么frida的知识,所以我们重点看fridahook
进入mainactivity,发现如下
1 | final int i = get_random() |
调用了random函数,意味着我们可以hook这个点。官方solution中的解法在我frida17.6.2中并不适用,没办法用spawn来hook,需要attach,脚本如下
1 | Java.perform(function() { |
在powershell中输入
1 | frida -U -n "Frida 0x1" -l "hook.js" |
由于我们是attach上的,所以我们需要让程序刷新一次,经搜索得知,当我们旋转虚拟机的屏幕后,虚拟机会销毁当前activity并重启,这样我们就能强行attach上了。又由源码中的计算公式得知,当我们return 5时,我们需要输入14,输入后得到flag

FridaLab-0x2
逻辑清晰的题,主逻辑如下
1 | public class MainActivity extends AppCompatActivity { |
可以看到当a=4919时app就会实现自解密。运行app回显如下

可以看到app并没有给出一个可以输入的地方,那么我们就只能通过frida强制赋值然后让程序输出flag。hook脚本如下
1 | Java.perform(function() { |
输入后可以看到app回显如下

便可以得到flag了
nctf-hookmysecret
虽然题目叫hookmysecret,但是暂时不知道哪里能够动态hook,我是直接静态分析的。首先丢进jadx中定位到主函数,发现是有三个阶段的程序,解压apk看native层后发现so文件中只有stage2的加密逻辑,那估计就只能一层一层来看。
首先看第一层,主要的逻辑如下
1 | // e.AbstractActivityC0110h, androidx.activity.k, z.f, android.app.Activity |
可以看到我们看不到什么,但是这里有一个 c 的类被调用了,我们追踪,其主要逻辑如下
1 | while (true) { |
可以看到这个逻辑主要的行为是对我们的输入进行SHA256加密再与目标字符串进行对比,虚拟机运行apk后发现是一个图形密码,又看到有 “,” 的逻辑,那应该是把我们输入的当作0~8,每个之间用逗号隔开,然后整体拿去SHA256加密,再结果与目标字符串进行对比。直接爆破就行,爆破脚本如下
1 | import hashlib |
运行即可得到结果为0,1,2,4,8,输入后发现正确即可绕过第一层。
进入第二层,可以找到如下逻辑
1 | hVar.z(str); |
其中可以看到在对比时调用了一个iArr = K0.a.f457a,我们追踪这个类可以看到
1 | package K0; |
大抵是第二层加密的目标数据,回到native层分析stage2的加密逻辑,进入native层后发现被严重混淆,抽离语句后核心的伪代码如下
1 | LABEL_27: |
简化之后如下
1 | int a=0x51; |
而目标密文就是在java层找到的数组,我们就可以得到逆向脚本如下
1 |
|
这里有个需要注意的点:从java层转过来的值在运算的时候最好是使用unit8_t来进行定义和计算,这样就可以避免编译器的自动截断
算出来的结果为k7Xm2Pq9Wv4N8bRt,经检验是对的,进入stage3。在Stage3Activity中我们并没有找到正常的逻辑,但是在发现stage2结果数组的下面发现了stage3的实现逻辑,是一个AESCBC加密。详细如下
1 | Context context = aVar.f475a; |
1 | package M0; |
厨子一把梭,结果如下

xm招新赛-MixTielele
首先照旧APK丢进JADX中看
1 | package com.example.titlele; |
进一步跟进
1 | package com.example.titlele; |
可以看到native层的调用,以及登入的user,进入native层,看到在进入IDA中时,这个so文件的界面是不一样的

那基本可以确定这个so文件不是正常的so,而是一个apk或者zip,改后缀丢进JADX中看看。尝试直接定位mainactivity,发现JADX报错说找不到,根据源程序的JADX界面中的目录在此so文件中找到逻辑,可以找到一些关键点
1 | package com.example.titlele; |
这里可以看到又出现了一个Login的逻辑,所以基本可以判定之前的apk的逻辑有问题,还出现了一个base64和LoginInfo,那大概我们传入的username和base64有关。
1 | package com.example.titlele; |
这里主要是调用,没有什么特殊之处。
1 | package com.example.utils; |
这里出现了一个线性同余加密算法。现在梳理一遍:
我们输入的字符串程序接收之后通过protobuf传输到这个apk中,然后字符串被转为字节数据,接着被传入线性同余算法中计算之后传入base64中进行加密,然后得到最终传入的输入。
接下来的分析转到libmixtitlele.so中。
1 | jint JNI_OnLoad(JavaVM *vm, void *reserved) |
可以看到出现了frida反调试和_JNIEnv::RegisterNatives注册函数,追踪参数off_20DCA8,可以定位到关键函数sub_D6DF8

继续追踪sub_D6DF8的逻辑,得到
1 | __int64 __fastcall sub_D6DF8(_JNIEnv *a1, __int64 a2, __int64 a3) |
追踪执行流可得,由程序随机生成一个16字节数据,然后被传进RSA中进行加密,同时被传进AESCBC128中加密,然后最终结果为{a1:RSA密文,b2:AES密文}
由于题目描述为"please login as admin"所以可得我们输入的username大概率就是admin,回看题目的最初的APK,发现地址http://120.48.104.4:2788/24ab99d75d3327cf3c46/login,通过POST协议将我们的最终结果发送到此地址中应该就能回显flag。
那我们的做法就应该是发送admin并且将ishacker改为false然后发送到服务端让其输出flag,但是问题来了,native层显示的是随机产生16字节的数据来进行计算key,我们完全不知道这个随机数产生的逻辑和我编译器该如何进行逆向呢?
其实这题我们需要的不是静态的硬分析,而是通过伪实现一个完整的流程让程序误以为我们的数据是正确的,因为程序程序并不会检查我们的key数据是否为随机产生的,只会检测RSA和AES的key是否为同一个key,然后去解密。所以我们只需要让我们所需的数据完整实现一次程序的流程发包到目标地址就可以让地址误以为我们成功得到了真正的数据从而得到flag。
那么逆向脚本如下
1 | import os |
以下是Rose的Fridahook脚本
1 | Java.perform(function () { |