Android逆向
教我兄弟学Android逆向
03 破解第一个Android游戏
切水果大作战
方法一
《新人贴》初次尝试破解内购小游戏:切水果大作战 - 『移动安全区』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn
正常打开搜索失败字符串即可

定位到找到对应的三个函数payResultSuccess和payResultFalse,payResultCancel

可以用jd-gui打开分析代码

同样搜索可以定位到对应的函数

然后在AndroidKill的smali代码窗口中将payResultSuccess的代码原封不动复制到payResultFalse和payResultCancel即可,最后编译即可。

方法二
可以通过修改跳转来使得任意点击均可跳转到payResultSuccess。
全局搜索payResultFalse,在MiGuSdkPay$1.smali中有对应的switch跳转语句

这里的
1 | .line 54 |
即为声明switch语句,跳转至pswitch_data_0,在这个结构语句里面具体进行跳转
1 | .line 54 |
那么就是值为0则跳转pswitch_0,为1则跳转pswitch_1
查看对应的java源代码

可以看到对应的一些跳转,依据选择不同,那么就在smali代码中修改对应的跳转即可,即将pswitch_0修改为pswitch_1即可。
08 IDA爆破签名验证
主要获取到可以导入jni.h的知识,在TIPS
09 IDA动态破解登陆验证
主要获取到IDA动态调试.so的知识,在TIPS
10 静态分析反调试apk
init_array和JNI_OnLoad会在so加载的时候就开始执行,所以程序也有可能会在这里开启线程进行反调试。
反调试点
checkport
在MainActivity中调用了

如果存在结果,即0x5d8a=23946端口被占用,那么就会exit,否则就挑战成功。
可以通过android_server -p23947设置端口来绕过
init_array
在.so文件加载时就会被运行,比JNI_OnLoad早

对应函数

跳转函数

即fget按行读取,总共6次遍历,获取
/proc/xx/status的第六行,即TracePid,查看里面的数据是否大于0,大于0则代表程序正在被调试,即退出。
可以刷机改内核,使得TracerPid永远为0
逆向修改内核,绕过TracerPID反调试 - 『移动安全区』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn
JNI_OnLoad函数
在执行
System.loadLibrary("xxx.so")时被调用
进入SearchObjProcess函数

查看是否有这些进程,有就退出
可以将android_server修改为其他名称
11 动态调试init_array
没成功,寄
不过调试最开始的需要在手机中这样运行才行,在Frida中好像也讲过
1 | am start -D -n demo2.jni.com.myapplication/.MainActivity |
安卓逆向这档事
课程二、初识APK文件结构、双开、汉化、基础修改
参考基础知识中的汉化知识块。
汉化未知语言
同时MT管理器还有一键翻译的功能,下载对应的翻译插件即可。
课后作业
汉化
可以先进行通用搜索

查找到resources.arsc,这个也是汉化大头

然后可以直接点击翻译模式

点击[DEFAULT]

找到对应字符串即可修改

应用双开
MT管理器就可以
替换图片
通过图片资源ID,在XML搜索中查找到的XML,一般是用来定义图片布局大小和加载路径的,可以通过修改这个布局达到去除图片和替换图片的目的

MT不显示资源ID时,可以点击如下设置,取消ID转名称,这样好查找

然后找到app:srcCompat即可,复制这个7f0d000b资源ID

随后到resources.arsc中的编辑器

通过ID定位资源

即可查看到对应的资源路径,然后替换图片即可

课程三、初识smali、VIP终结者
有些字符串是一个资源,不是写在代码里面,使用开发者助手查找时通常为0x7xxxxx这样的类型,那么就在MT管理器里面反编译classes.dex用整型的16进制进行搜索


找到修改即可

课程四、恭喜获得广告&弹窗静默卡
获取Activity记录
可以用MT管理器来获取一个APK启动的Activity的记录,在MT主界面左上角点击三横线图标展开,启动Activity记录服务

然后打开对应的APK,切换回MT管理器即可查看到

去除广告
对于广告来说,去除的方法通常有如下几种
第一种:修改加载时间,把广告的显示时间修改为0秒即可
获取广告
Activity的名称,反编译classes.dex进行类名搜索
然后找到类的
smali路径进行搜索
再进行代码搜索

进入到如下的
AdActivity类中代码进行分析
即在主函数
onCreate中调用了广告代码loadAd,随后启动广告,持续3000ms1
2
3
4
5
6
7
8
9
10protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(2131427362);
loadAd();
}
//....
private final void loadAd() {
this.timeoutHandler.removeMessages(MSG_AD_TIMEOUT);
this.timeoutHandler.sendEmptyMessageDelayed(MSG_AD_TIMEOUT, 3000);
}那么将
3000ms调成0ms即可跳过广告。第二种:修改
Activity执行顺序,跳过广告的Activity这个最好还是不用,只需要在
AndroidManifest.xml中将主Activity修改为自己需要的就行,比如这里就直接将主Activity修改为第三关的Activity。但是很多的Activity都会加载资源,直接跳转到后面的Acitivity容易导致应用GG。第三种:修改切换
Activity的代码即找到调用
Activity的代码,将调用广告Activity处的代码直接修改为调用第三关的Acitvity首先还是查找一下广告
Activity,然后需要排查一下,本身的Activity去除一下,查找调用到广告的Activity,排查找到com.zj.wuaipojie.ui.Adapter中有调用
进入查看定位关键代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25private static final void m0onCreateViewHolder$lambda-0(ViewHolder viewHolder, View view) {
int adapterPosition = viewHolder.getAdapterPosition();
Intent intent;
if (adapterPosition == 0) {
intent = new Intent();
intent.setClass(view.getContext(), ChallengeFirst.class);
view.getContext().startActivity(intent);
} else if (adapterPosition == 1) {
intent = new Intent();
intent.setClass(view.getContext(), ChallengeSecond.class);
view.getContext().startActivity(intent);
} else if (adapterPosition == 2) {
intent = new Intent();
intent.setClass(view.getContext(), AdActivity.class);
view.getContext().startActivity(intent);
} else if (adapterPosition == 3) {
intent = new Intent();
intent.setClass(view.getContext(), ChallengeFourth.class);
view.getContext().startActivity(intent);
} else if (adapterPosition == 4) {
intent = new Intent();
intent.setClass(view.getContext(), ChallengeFifth.class);
view.getContext().startActivity(intent);
}
}可以看到这里进入第三关时会先加载广告
Acitivity,那么这里把进入第三关的代码修改为直接进入第三关的Activity即可,在smali代码中进行修改,将Lcom/zj/wuaipojie/ui/AdActivity修改为Lcom/zj/wuaipojie/ui/ChallengeSecond即可。
然后保存重新安装即可。
弹窗修改
最主要的是定位弹窗在哪。
针对更新弹窗而言,一般都是在代码逻辑里判断新版本和当前版本是否版本号一致,那么可以直接修改XML中的versiocode为最新版本即可

还有的可以用算法助手工具进行直接Hook,有弹窗定位,关键词屏蔽功能等,这样就可以把某些返回键禁止的弹窗给hook了。
修改
dex代码使用开发助手的日志功能对弹窗进行定位,找到对应的代码,注释掉
show方法修改即可。
抓包修改响应体
使用开发助手->布局查看功能找到横幅广告图片的view Id在哪,修改高度宽度即可


随后进行
XML搜索即可,修改长宽。
另外也可以加入一段代码
1
android:visibility="gone"
课后作业
使用算法助手的弹窗定位功能,定位到关键函数,这个函数是被混淆过的函数。

复制这个方法名称,在MT中搜索

定位之后,里面有三个函数都会有弹窗调用

都把那个show给注释掉即可,或者查找调用点,在调用点处注释即可
或者使用关键字”弹窗”来定位代码也是一样的。
课程五、1000-7=?&动态调试&Log插桩
动态调试
首先需要将
AndroidManifest.xml中加入调试权限
然后重新安装,再把安装包放入
JEB进行分析,在需要下断点的地方按ctrl + b即可下断点。随后使用
adb连上手机,输入如下命令开启调试功能1
adb shell am start -D -n com.zj.wuaipojie/.ui.MainActivity
命令解析为
adb shell am start -D -n
adb shell am start -D -n 包名/类名
am start -n 表示启动一个activity
am start -D 表示将应用设置为可调试模式这个类名为主类名才行。
输入命令后,在手机中会显示如下弹窗

之后点击
JEB上的调试即可
其他的调试方法有点麻烦,不研究了。
Log插桩
- 将插桩的
.dex文件添加到apk中,长按出现添加按钮



随后在需要获取的寄存器值的位置,加入如下代码
1
invoke-static {对应寄存器}, Lcom/mtools/LogUtils;->v(Ljava/lang/Object;)V
如下图所示

获取
v0的值
课后作业
算法助手启用onClick监听,分析到是在MainActivity中,或者通过字符串搜索定位资源ID,然后定位资源ID调用的代码

那么在该类中查找一下onClick,可以找到对应的函数

直接让checkSN永远返回1即可,在MT管理器中查找修改,添加如下即可

动态调试的话,也还是有点麻烦,拉到把。
课程六、校验的N次方-签名校验对抗、PM代{过}{滤}理、IO重定向
检测校验
签名校验
通常就是校验包的hash值是否被修改
普通签名校验
系统将应用的签名信息封装在 PackageInfo 中,调用 PackageManager 的 getPackageInfo(String packageName, int flags) 即可获取指定包名的签名信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14private boolean SignCheck() {
String trueSignMD5 = "d0add9987c7c84aeb7198c3ff26ca152";
String nowSignMD5 = "";
try {
// 得到签名的MD5
PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(),PackageManager.GET_SIGNATURES);
Signature[] signs = packageInfo.signatures;
String signBase64 = Base64Util.encodeToString(signs[0].toByteArray());
nowSignMD5 = MD5Utils.MD5(signBase64);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return trueSignMD5.equals(nowSignMD5);
}这种签名一般在签名校验的部分,注释掉签名校验错误的代码,或者将那个
trueSignMD5签名修改为实际签名即可。校验Application
CRC校验
获取dex的crc的值,即当修改了dex,它的值必定会发生改变
1
2ZipEntry entry = new ZipFile(getPackageCodePath()).getEntry("classes.dex");
Log.e("zj2595", "dexCrc:" + entry.getCrc());hash校验
获取整个APK的hash值,然后进行比对
root检测
检测方式
检查设备的
build tags是否包含test-keys。这通常是用于测试的设备,因此如果检测到这个标记,则可以认为设备已被 root。检查设备是否存在一些特定的文件,这些文件通常被用于执行 root 操作。如果检测到这些文件,则可以认为设备已被 root。比如如下一些常见的
1
"/system/app/Superuser.apk", "/sbin/su", "/system/bin/su", "/system/xbin/su", "/data/local/xbin/su", "/data/local/bin/su", "/system/sd/xbin/su","/system/bin/failsafe/su", "/data/local/su", "/su/bin/su
执行which su,来查看是否有输出
对抗手段
- IO重定向使文件不可读
- 修改Android源码,去除常见指纹
- 算法助手等Hook
模拟器检测
检测方式
通过检测系统的 Build 对象来判断当前设备是否为模拟器。具体方法是检测 Build.FINGERPRINT 属性是否包含字符串 "generic"。
对抗手段
签名定位
- 一般通过算法助手定位闪退的日志
签名校验对抗
一键过签名工具:
MT、NP、ARMPro、CNFIX、Modex
PM代理
来源于:https://github.com/fourbrother/HookPmsSignature
使用PM手撕没成功,不是很会
smali.jar:从smali转为dex
1
java -jar smali.jar assemble -o 输出目录 输入目录
baksmali.jar:从dex转为smali
1
java -jar baksmali.jar d classes.dex
IO重定向
不签名安装
签名对抗还是手撕签名好一点,可以用一些hook助手来查找签名
PM代理有点过时,但是对于某些应用签名还是可以搞定的。
普通签名
可以用算法助手进行拦截
PM代理
首先PmsHookBinderInvocationHandler.smali和ServiceManagerWraper.smali
修改ServiceManagerWraper.smali中的内容,将签名信息和包名修改为指定安装包信息
签名信息为如下原始数据内容

然后将两个smali文件放到一个文件夹,使用命令
1
java -jar smali.jar assemble -o HookPMS.dex .\zhengji\Hook\
生成得到HookPMS.dex
随后将HookPMS.dex导入安装包,重新命名classes2.dex,重新打包

随后搜索attachBaseContext方法,在MainApplication中

加入如下代码
1
invoke-static {p1}, Lzhengji/Hook/ServiceManagerWraper;->hookPMS(Landroid/content/Context;)V
注意要和HookPMS.dex中的路径一致
Pixel4不行,用模拟器可以跑通

IO重定向
功能
- 让文件只读不可写
- 禁止访问文件
- 路径替换
用处
- 过签名校验,当校验时让它读取原包
- 风控对抗,某些APP会记录启动次数
- 过Root检测、Xposed检测
原理
hook了底层打开文件的一些路径
1 | using namespace std; |
具体实现
查找到check_crc的调用地方
点击罗盘

长按check_crc

即可找到目的方法

然后在开头写入如下代码,注意这个只是调用代码,实际的重定向的代码是一个so中的代码,作者已经写入到安装包了,我们只需要调用即可,后续需要自己将so打入apk
1 | sget-object p10, Lcom/zj/wuaipojie/util/ContextUtils;->INSTANCE:Lcom/zj/wuaipojie/util/ContextUtils; |
随后在软件数据目录中新建files文件夹,放入原安装包

重新命名为base.apk

在我的测试中,hash可以过,crc过不了
反调试
反调试方法
安卓自带的
1
2
3
4
5
6fun checkForDebugger() {
if (Debug.isDebuggerConnected()) {
// 如果调试器已连接,则终止应用程序
System.exit(0)
}
}debuggable属性
先检测手机的debuggable属性,没有检测到再检测APK内部的xml的那个debuggable属性
ptrace检测
1
2
3
4int ptrace_protect()//ptrace附加自身线程 会导致此进程TracerPid 变为父进程的TracerPid 即zygote
{
return ptrace(PTRACE_TRACEME,0,0,0);;//返回-1即为已经被调试
}调试进程名称检测
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
34int SearchObjProcess()
{
FILE* pfile=NULL;
char buf[0x1000]={0};
pfile=popen("ps","r");
if(NULL==pfile)
{
//LOGA("SearchObjProcess popen打开命令失败!\n");
return -1;
}
// 获取结果
//LOGA("popen方案:\n");
while(fgets(buf,sizeof(buf),pfile))
{
char* strA=NULL;
char* strB=NULL;
char* strC=NULL;
char* strD=NULL;
strA=strstr(buf,"android_server");//通过查找匹配子串判断
strB=strstr(buf,"gdbserver");
strC=strstr(buf,"gdb");
strD=strstr(buf,"fuwu");
if(strA || strB ||strC || strD)
{
return 1;
// 执行到这里,判定为调试状态
}
}
pclose(pfile);
return 0;
}
反调试对抗
https://bbs.pediy.com/thread-268155.htm
frida检测
https://github.com/xxr0ss/AntiFrida
课后作业
普通签名校验:
重新签名安装,检测到应用退出,开启算法助手退出拦截和日志捕获,定位到com.zj.wuaipojie.ui.ChallengeFifth.onCreate

然后分析onCreate,存在checkSign和root检测

剩下的分析一下,把checkSign返回为True即可,即在return之前始终赋值返回值为1

root检测部分也是一样的,返回值赋值为0即可
API校验:
即获取包的签名信息,转换为字符串之后进行base64编码,然后md5输出

如下所示

要么可以重定向,要么进行PM代理,或者直接改代码,返回值修改为1即可
CRC校验:
详见代码

即校验classes.dex文件,这里通用可以返回值赋值为1解决。
但是这里有个try-catch,并且catch始终返回为true。那么可以尝试让try中出错,进而跳转到catch,这里让classes.dex字符串修改为classes2.dex,这样找不到这个文件自然会出错,跳转到catch。
PM校验:详见PM代理部分
Hash校验:

直接用返回1解决
SO层校验

修改if条件语句即可,if-eqz修改为if-nez

整体通过
课程七/八、Sorry,会Hook真的可以为所欲为-Xposed快速上手
环境配置
创建空白的android项目,使用java和android7作为目标

复制xposed的jar包到项目的libs目录


修改xml文件配置,src->main->AndroidManifest.xml,修改如下
1 |
|
修改build.gradle.kts
1 | implementation(files("libs\\XposedBridgeAPI-89.jar")) |
区别在于implementation 使用该方式依赖的库将会参与编译和打包 compileOnly 只在编译时有效,不会参与打包
然后app新建目录,new->Folder->Assets Folder

assets目录下创建文件,为模块入口

app->src->java->com下新建hook类

然后需要在xposed_init中指定hook类入口

继承了IXposedHookLoadPackag便拥有了hook的能力,然后在Hook.java里面写代码就行

HOOK代码
hook指定包
首先指定需要hook的包
1 | public class Hook implements IXposedHookLoadPackage { |
添加了if语句代表只hook掉com.zj.wuaipojie这个apk
hook指定函数
在jadx中可以选定某个方法,复制为xposed片段,比如这里选择hook掉a方法

复制之后放入if语句后面即可,需要导入包,然后在修改classLoader,添加前缀loadPackageParam.classLoader
1 | XposedHelpers.findAndHookMethod("com.zj.wuaipojie.Demo", loadPackageParam.classLoader, "a", java.lang.String.class, new XC_MethodHook() { |
即传入类,classLoader,方法名称,参数类型,然后重写beforeHookedMethod和afterHookedMethod回调函数即可
或者直接用反射来获取,通常可以用来hook自定义或者复杂函数
1 | Class a = loadPackageParam.classLoader.loadClass("类名"); |
参数处理
在beforeHookedMethod中处理参数
1 | String a = "aaaaaaaa"; |
在afterHookedMethod中处理返回值
1 | Log.d("zj2595",param.getResult().toString()); //获取返回值 |
替换函数
类似的
1 | Class a = classLoader.loadClass("类名") |
通杀大部分免费加壳
针对一些加固的APP,需要先获取到classloader才能接着hook
1 | XposedHelpers.findAndHookMethod(Application.class, "attach", Context.class, new XC_MethodHook() { |
处理变量
没有的就用Object
string->Object
静态变量
static的,类被初始化,同步进行初始化
1
2final Class clazz = XposedHelpers.findClass("类名", loadPackageParam.classLoader);
XposedHelpers.setStaticIntField(clazz, "变量名", 999);实例变量
类被实例化(产生一个对象的时候),进行初始化
1
2
3
4
5
6
7
8
9
10final Class clazz = XposedHelpers.findClass("类名", classLoader);
XposedBridge.hookAllConstructors(clazz, new XC_MethodHook() { //先hook构造函数
protected void afterHookedMethod(MethodHookParam param) throws Throwable { //在构造函数初始化变量之后
super.afterHookedMethod(param);
//param.thisObject获取当前所属的对象
Object ob = param.thisObject;
XposedHelpers.setIntField(ob,"变量名",9999);
}
});
处理构造函数
1 | //无参构造函数,没有传入参数 |
处理Dex文件
一个dex中最多只能存在65535个方法,如果超过这个数,就会重新生成dex文件
1 | XposedHelpers.findAndHookMethod(Application.class, "attach", Context.class, new XC_MethodHook() { |
函数主动调用
静态函数
1
2Class clazz = XposedHelpers.findClass("类名",loadPackageParam.classLoader);
XposedHelpers.callStaticMethod(clazz,"方法名",参数(非必须));实例函数
1
2Class clazz = XposedHelpers.findClass("类名",loadPackageParam.classLoader);
XposedHelpers.callMethod(clazz.newInstance(),"方法名",参数(非必须));
内部类
类中还有一个class,其实就是加入$符号来寻找类
1 | XposedHelpers.findAndHookMethod("com.zj.wuaipojie.Demo$InnerClass", loadPackageParam.classLoader, "innerFunc",String.class, new XC_MethodHook() { |
反射调用
即先hook住某个函数,然后找到类之后对应调用即可
1 | Class clazz = XposedHelpers.findClass("com.zj.wuaipojie.Demo", loadPackageParam.classLoader); |
遍历包中所有类下所有方法
1 | XposedHelpers.findAndHookMethod(ClassLoader.class, "loadClass", String.class, new XC_MethodHook() { |
堆栈查询
定位”已过期”字符串赋值部分的堆栈
1 | XposedHelpers.findAndHookMethod("android.widget.TextView", loadPackageParam.classLoader, "setText", CharSequence.class, new XC_MethodHook() { |
事件监听
检测点击事件对应的方法
1 | Class clazz = XposedHelpers.findClass("android.view.View", loadPackageParam.classLoader); |
布局改写
1 | XposedHelpers.findAndHookMethod("com.zj.wuaipojie.ui.ChallengeSixth", loadPackageParam.classLoader, |
LSPosed免root
使用https://github.com/LSPosed/LSPatch即可
先安装模块,然后打开LPatch,在应用栏添加应用

选择已安装的APK,或者存储目录都行
本地模式:只能在本地设备运行,相当于还是hook
集成模式:将模块嵌入到APP中,可以在其他手机运行,这个其实也会修改签名

点击嵌入模块,然后选择对应的模块即可

覆写版本号,选择签名强度开始修补即可

修补后会在之前选定的存储目录中生成对应的APP,此时安装即可。
简单HOOK
有一些简单hook的教程,还有Xposed的源码解析
GitHub - littleWhiteDuck/SimpleHook: SimpleHook hook部分代码
十、不是我说,有了IDA还要什么女朋友?
ELF就不介绍了
NDK开发
在Frida逆向与抓包实战有介绍
课后作业
如之前所分析的,调用的是so里面的getSercet函数,打开IDA分析,修改函数如下返回值

修改为MOV W0,1即可,再用MT导入apk包重新安装即可。

十二、大佬帮我分析一下
so文件加载流程

| 函数名 | 描述 |
|---|---|
android_dlopen_ext() 、dlopen()、do_dlopen() |
这三个函数主要用于加载库文件。android_dlopen_ext 是系统的一个函数,用于在运行时动态加载共享库。与标准的 dlopen() 函数相比,android_dlopen_ext 提供了更多的参数选项和扩展功能,例如支持命名空间、符号版本等特性。 |
find_library() |
find_library() 函数用于查找库,基本的用途是给定一个库的名字,然后查找并返回这个库的路径。 |
call_constructors() |
call_constructors() 是用于调用动态加载库中的构造函数的函数。 |
init |
库的构造函数,用于初始化库中的静态变量或执行其他需要在库被加载时完成的任务。如果没有定义init函数,系统将不会执行任何动作。需要注意的是,init函数不应该有任何参数,并且也没有返回值。 |
init_array |
init_array是ELF(Executable and Linkable Format,可执行和可链接格式)二进制格式中的一个特殊段(section),这个段包含了一些函数的指针,这些函数将在main()函数执行前被调用,用于初始化静态局部变量和全局变量。 |
jni_onload |
这是Android JNI(Java Native Interface)中的一个函数。当一个native库被系统加载时,该函数会被自动调用。JNI_OnLoad可以做一些初始化工作,例如注册你的native方法或者初始化一些数据结构。如果你的native库没有定义这个函数,那么JNI会使用默认的行为。JNI_OnLoad的返回值应该是需要的JNI版本,一般返回JNI_VERSION_1_6。 |
下断点时机
应用级别的:java_com_XXX;
外壳级别的:JNI_Onload,.init,.init_array(反调试);
系统级别的:fopen,fget,dvmdexfileopen(脱壳);
调试步骤
1 | adb shell am start -D -n com.zj.wuaipojie/.ui.ChallengeEight (去掉-D 则表示不以debug模式启动app) |
PS:若不是以debug启动则不需要输入后两条命令
有时候so文件是在apk点击登录或者什么操作之后才会加载调用,这时候就不需要adb shell am start进行启动,在手机启动apk然后开启端口转发,附加就行,比较不容易出错。
SO防护手段
常见防护手段:
| 主要功能 | 描述 |
|---|---|
| SO加壳 | 对C/C++源码编译出来的SO文件进行加壳,使SO文件无法正确反编译和反汇编。 |
| SO源码虚拟化保护 | 将原始汇编指令翻译为自定义的虚拟机指令,跳转到自定义的虚拟机中执行,每次保护生成的虚拟机指令随机,且对虚拟机解释器再度混淆 |
| SO防调用 | 对SO文件进行授权绑定,防止SO文件被非授权应用调用运行。 |
| SO Linker | 对整个SO文件进行加密压缩,包括代码段、符号表和字符串等,运行时再解密解压缩到内存,从而有效的防止SO数据的泄露。 |
| SO源码混淆 | 常量字符串加密、分裂基本块、等价指令替换、虚假控制流、控制流平坦化。 |
| SO环境监测 | 防frida\xposed\root、防动态调试、防模拟器、防多开等 |
OLLVM具体分析相关部分放在IDA逆向技巧了,包括交叉引用、Trace等等
十三、是时候学习一下Frida一把梭了(上)
Frida原理
frida注入的原理就是找到目标进程,使用ptrace跟踪目标进程获取mmap,dlpoen,dlsym等函数库的偏移获取mmap在目标进程申请一段内存空间将在目标进程中找到存放frida-agent-32/64.so的空间启动执行各种操作由agent去实现
| 组件名称 | 功能描述 |
|---|---|
| frida-gum | 提供了inline-hook的核心实现,还包含了代码跟踪模块Stalker,用于内存访问监控的MemoryAccessMonitor,以及符号查找、栈回溯实现、内存扫描、动态代码生成和重定位等功能 |
| frida-core | fridahook的核心,具有进程注入、进程间通信、会话管理、脚本生命周期管理等功能,屏蔽部分底层的实现细节并给最终用户提供开箱即用的操作接口。包含了frida-server、frida-gadget、frida-agent、frida-helper、frida-inject等关键模块和组件,以及之间的互相通信底座 |
| frida-gadget | 本身是一个动态库,可以通过重打包修改动态库的依赖或者修改smali代码去实现向三方应用注入gadget,从而实现Frida的持久化或免root |
| frida-server | 本质上是一个二进制文件,类似于前面学习到的android_server,需要在目标设备上运行并转发端口,在Frida hook中起到关键作用 |
环境配置参考《Frida逆向与抓包实战》
操作模式
即CLI命令行和RPC使用js代码注入两种模式
注入模式
spawn主动启动模式和attach附加模式
基础语法
| API名称 | 描述 |
|---|---|
Java.use(className) |
获取指定的Java类并使其在JavaScript代码中可用。 |
Java.perform(callback) |
确保回调函数在Java的主线程上执行。 |
Java.choose(className, callbacks) |
枚举指定类的所有实例。 |
Java.cast(obj, cls) |
将一个Java对象转换成另一个Java类的实例。 |
Java.enumerateLoadedClasses(callbacks) |
枚举进程中已经加载的所有Java类。 |
Java.enumerateClassLoaders(callbacks) |
枚举进程中存在的所有Java类加载器。 |
Java.enumerateMethods(targetClassMethod) |
枚举指定类的所有方法。 |
日志输出语法
| 日志方法 | 描述 | 区别 |
|---|---|---|
console.log() |
使用JavaScript直接进行日志打印 | 多用于在CLI模式中,console.log()直接输出到命令行界面,使用户可以实时查看。在RPC模式中,console.log()同样输出在命令行,但可能被Python脚本的输出内容掩盖。 |
send() |
Frida的专有方法,用于发送数据或日志到外部Python脚本 | 多用于RPC模式中,它允许JavaScript脚本发送数据到Python脚本,Python脚本可以进一步处理或记录这些数据。 |
Hook代码
通过logcat |grep "D.zj2595"日志捕获
框架模板
1 | function main(){ |
hook类普通方法
如下即重新实现com.zj.wuaipojie.Demo.a函数,打印参数和返回值
1 | //定义一个名为hookTest1的函数 |
在这里面修改参数,return返回值就可以修改这个函数了。
hook重载函数
如果方法是重载的,那么需要设置一下具体的参数,比如上面的函数a是重载的,那么需要设定如下
1 | function hookTest1(){ |
即添加overload指定对应重载函数的参数来确定该函数
hook重载自定义参数类型的函数
借助smali代码来查看,比如如下Inner函数中的Animal参数类型

切换为smali代码,第一个参数类型即为com.zj.wuaipojie.Demo$Animal

对应hook代码如下,修改第二个参数为”aaaaaaaaaa”
1 | function hookTest1(){ |
hook构造函数
用$init来表示构造函数,同样重载的构造函数也是类似的
1 | function hookTest3(){ |
hook变量
- 静态变量
1 | utils.staticField.value = "我是被修改的静态变量"; |
- 实际变量
注意字段名与函数名相同 前面加个下划线
1 | Java.choose("com.zj.wuaipojie.Demo", { |
实际上在这个例子里面,大佬说hoos是发生在obj.test()函数调用之后的,但是也很奇怪啊,如果是真的,那么应该是obj.test()函数之前的所有数据均无法被更改,反正很奇怪。
hook内部类
一样的,就是获取到目标类就行
1 | function hookTest6(){ |
枚举所有类与所有方法
1 | function hookTest7(){ |
枚举所有方法
1 | function hookTest8(){ |
主动调用
静态方法
1
2var ClassName=Java.use("com.zj.wuaipojie.Demo");
ClassName.privateFunc("传参");非静态方法
和变量类似,都是要获取对应类的对象
1
2
3
4
5
6
7
8
9
10
11
12var ret = null;
Java.perform(function () {
Java.choose("com.zj.wuaipojie.Demo",{ //要hook的类
onMatch:function(instance){
ret=instance.privateFunc("aaaaaaa"); //要hook的方法
},
onComplete:function(){
//console.log("result: " + ret);
}
});
})
//return ret;
十四、是时候学习一下Frida一把梭了(中)
Objection用法
objection是基于frida的命令行hook集合工具, 可以让你不写代码, 敲几句命令就可以对java函数的高颗粒度hook, 还支持RPC调用。可以实现诸如内存搜索、类和模块搜索、方法hook打印参数返回值调用栈等常用功能,是一个非常方便的,逆向必备、内存漫游神器。
常用API
objection -g 包名 explore:即开始探索,会直接启动应用

objection -g 进程名 explore –startup-command “android hooking watch class 路径.类名” :代表启动应用前就开始Hook,命令为后面的那个

memory list modules -查看内存中加载的库

memory list exports so名称 - 查看库的导出函数

android hooking list activities -查看内存中加载的activity /android

android hooking list services -查看内存中加载的services,好像不太兼容

android intent launch_activity activity名称:可以直接拉起某个activity

关闭ssl校验:android sslpinning disable

关闭root校验:android root disable

Objection内存漫游
从内存中查看数据
android heap search instances 类名(命令):查看某个类对象
这是内存中没加载的情况

有加载的是如下情况

android heap execute [hashcode] getPublicInt(实例的hashcode+方法名) :调用实例的方法
无参函数

有参函数
进入evaluate编辑界面,clazz.a(“ssss”),即调用clazz这个实例中的a方法,传入参数为”ssss”,然后按照提示按ESC,然后Enter即可

android hooking list classes -列出内存中所有的类(结果比静态分析的更准确),用的不多

android hooking search classes 关键类名 -在内存中所有已加载的类中搜索包含特定关键词的类,以
wuaipojie为关键词
android hooking search methods 关键方法名 -在内存中所有已加载的类的方法中搜索包含特定关键词的方法(一般不建议使用,特别耗时,还可能崩溃)
android hooking list class_methods 类名 -内存漫游类中的所有方法

Objection的HOOK
这个HOOK并不会使得这些方法失效,相当于只是查看
android hooking watch class 类名 :hook类的所有方法

android hooking watch class_method 类名.方法名 –dump-args –dump-return –dump-backtrace :hook方法的参数、返回值和调用栈

android hooking watch class_method 类名.$init :hook构造函数

十五、是时候学习一下Frida一把梭了(下)
打印导入表/导出表
貌似需要frida14,frida16可能写法有点出入
1 | function hookTest1(){ |
静态注册JNI函数HOOK
JAVA层,同样也是获取到类之后重新实现一下函数就行
1
2
3
4
5
6
7
8
9function hookTest1(){
let SecurityUtil = Java.use("com.zj.wuaipojie.util.SecurityUtil");
SecurityUtil["checkVip"].implementation = function(){
console.log("checkVip is called!");
let ret = this.checkVip();
console.log("checkVip ret value is " + ret);
return true;
};
}SO层,即需要通过函数名称获取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23function hookTest2(){
Java.perform(function(){
//根据导出函数名打印地址
var helloAddr = Module.findExportByName("lib52pojie.so","Java_com_zj_wuaipojie_util_SecurityUtil_checkVip");
console.log(helloAddr);
if(helloAddr != null){
//Interceptor.attach是Frida里的一个拦截器
Interceptor.attach(helloAddr,{
//onEnter里可以打印和修改参数
onEnter: function(args){ //args传入参数
console.log(args[0]); //打印第一个参数的值
},
//onLeave里可以打印和修改返回值
onLeave: function(retval){ //retval返回值
console.log(retval);
console.log("retval",retval.toInt32());
retval.replace(1);
}
})
}
})
}
SO层HOOK打印
寄存器
1
console.log(this.context.x1); // 打印寄存器内容
整数
1
console.log(args[1].toInt32()); //toInt32()转十进制
Char字符形式
1
console.log(args[2].readCString()); //读取字符串 char类型
字符串形式
1
2
3
4
5
6
7
8// 方法一
var jString = Java.cast(args[2], Java.use('java.lang.String'));
console.log("参数:", jString.toString());
//方法二
var JNIEnv = Java.vm.getEnv();
var originalStrPtr = JNIEnv.getStringUtfChars(args[2], null).readCString();
console.log("参数:", originalStrPtr);内存DUMP
1
console.log(hexdump(args[2])); //内存dump

SO层HOOK修改
整数
1
2
3
4
5//修改参数
args[0] = ptr(1000); //第一个参数修改为整数 1000,先转为指针再赋值
//修改返回值
retval.replace(20000); //返回值修改字符串修改
1
2
3
4
5
6
7
8
9//修改参数
var modifiedContent = "至尊";
var newJString = JNIEnv.newStringUtf(modifiedContent);
args[2] = newJString;
//修改返回值
var modifiedContent = "无敌";
var newJString = JNIEnv.newStringUtf(modifiedContent);
retval.replace(newJString);
SO基地址获取
1 | var moduleAddr1 = Process.findModuleByName("lib52pojie.so").base; |
SO未导出函数与函数地址计算
可能存在函数运行,但是没有设置JNI静态导出类型的,只是在SO层内部使用,这时候可以通过加载地址来调用,同样对于JNI动态注册类型也适用。
安卓里一般32 位的 so 中都是
thumb指令,64 位的 so 中都是arm指令通过IDA里的opcode bytes来判断
arm 指令为 4 个字节(options -> general -> Number of opcode bytes (non-graph) 输入4)
thumb指令不确定的,有的4字节,有的2字节
地址计算:
thumb 指令,函数地址计算方式: so 基址 + 函数在 so 中的偏移 +1
arm 指令,函数地址计算方式: so 基址 + 函数在 so 中的偏移
1 | function hookTest6(){ |
Hook掉dlopen
dlopen是用来加载so文件的

即可以通过hook时判断参数filename是否为我们想要的.so文件,来指定Hook到.so文件加载时运行的函数逻辑,这里即为关注hook加载lib52pojie.so文件的函数逻辑
1 | function hook_dlopen() { |
十六、是时候学习一下Frida一把梭了(终)
Frida写数据
一般用在脱壳当中
1 | //一般写在app的私有目录里,不然会报错:failed to open file (Permission denied)(实际上就是权限不足) |
inlinehook内联
写法和函数地址计算时的写法一样的,其实就是定位到某条汇编,然后进行Hook。
我感觉函数层面的原理就是基于这个的,只是当检测为函数时,就可以操作args了。而且这个onLeave,可能检测的是汇编 RET指令,然后修改返回的X0寄存器作为返回值,因为在这个例子里面,onLeave也能修改适用。
这里Hook的是check函数中的如下位置

1 | function inline_hook(){ |
这里Hook到1042C,然后给X0赋值为1也是一样的。那么其实对于Frida针对So层的Hook,其实具有更加细腻的汇编层面,那这个可以修改汇编代码吗?
汇编层面
[Online ARM to HEX Converter (armconverter.com)](https://armconverter.com/?code=mov w0,1)
读取
1
2
3var soAddr = Module.findBaseAddress("lib52pojie.so");
var codeAddr = Instruction.parse(soAddr.add(0x10428));
console.log(codeAddr.toString());

写入
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
28function hexToBytes(str) {
var pos = 0;
var len = str.length;
if (len % 2 != 0) {
return null;
}
len /= 2;
var hexA = new Array();
for (var i = 0; i < len; i++) {
var s = str.substr(pos, 2);
var v = parseInt(s, 16);
hexA.push(v);
pos += 2;
}
return hexA;
}
function compile(){
var soAddr = Module.findBaseAddress("lib52pojie.so");
var codeAddr = Instruction.parse(soAddr.add(0x10428));
console.log(codeAddr.toString());
Memory.patchCode(codeAddr, 4, function(code) {
const writer = new Arm64Writer(code, { pc: codeAddr });
writer.putBytes(hexToBytes("20008052"));
writer.flush();
});
}会报错
1
2
3
4
5
6
7
8
9
10
11
12
13Error: expected a pointer
at value (frida/runtime/core.js:207)
at compile (/root/Desktop/CTF/AndroidRevFrida/test.js:161)
at <anonymous> (/root/Desktop/CTF/AndroidRevFrida/test.js:166)
at <anonymous> (frida/node_modules/frida-java-bridge/lib/vm.js:12)
at _performPendingVmOps (frida/node_modules/frida-java-bridge/index.js:250)
at <anonymous> (frida/node_modules/frida-java-bridge/index.js:225)
at <anonymous> (frida/node_modules/frida-java-bridge/lib/vm.js:12)
at _performPendingVmOpsWhenReady (frida/node_modules/frida-java-bridge/index.js:244)
at perform (frida/node_modules/frida-java-bridge/index.js:204)
at main (/root/Desktop/CTF/AndroidRevFrida/test.js:169)
at apply (native)
at <anonymous> (frida/runtime/core.js:51)
SO层函数主动调用
这里即主动调用AES加密,参数和返回值均为字符串指针,那么就写pointer
1 | var funcAddr = Module.findBaseAddress("lib52pojie.so").add(0xe85c); |
对应的数据类型如下
参考JavaScript API | Frida • A world-class dynamic instrumentation toolkit
| 数据类型 | 描述 |
|---|---|
| void | 无返回值 |
| pointer | 指针 |
| int | 整数 |
| long | 长整数 |
| char | 字符 |
| float | 浮点数 |
| double | 双精度浮点数 |
| bool | 布尔值 |
番外
去除签名校验
手动去除,搜索app kill,找到第一个,进入查找f函数,修改f函数后保存
1 | .method public static f()Ljava/util/List; |
去除更新弹窗
安卓逆向入门笔记
https://www.52pojie.cn/thread-1781093-1-1.html
1.修改默认Activity
a)解包
使用apktool进行反编译,apktool d xxx.apk。将class.dex从apk中解包出来,用d2j-dex2jar class.dex转换为class-dex2jar.jar包
b)添加Activity/布局文件
手写Activity,保存为myActivity.smali
1 | ###### Class com.p005zj.wuaipojie.p006ui.MyActivity (com.zj.wuaipojie.ui.MyActivity) |
对应java代码为
1 | import android.content.Intent; |
找到apk中的存放activity的位置,放入myActivity,这个包里的路径为:教程demo(更新)\smali\com\zj\wuaipojie\ui
然后添加布局文件hello_word.xml,放到教程demo(更新)\res\layout\下
1 |
|
添加布局文件,那么还需要添加布局文件的的资源ID,在教程demo(更新)\smali\com\zj\wuaipojie\R$layout.smali文件中添加如下

需要确保这个id,即0x7f0b007e在教程demo(更新)\res\values\public.xml中是没有的,然后在对应layout标签的最下面添加即可

c)声明Activity并默认启动
修改AndroidManifest.xml,将myActivity设置为主Activity

使用apktool进行编译,apktool b xxx -o xxx,提示如下错误

换成最新版本的可以成功,或者用AndroidKiller,替换一下apktool就行,但是不要改脚本,改apktool的文件名就可以。
d)签名
正常签名
完成之后,安装
Frida逆向与抓包实战
参考的书《Frida逆向与抓包实战》
环境安装
还是用bluestacks5,版本为android9,bluestacks.conf不要设置只读,不然adb的端口也被固定,这样windows启动之后说不定就占了这个端口,这样就不太行了。
然后windows安装对应的python、frida、frida-tools即可,这里用的是python3.8.0
1 | pip install frida==12.8.0 |
然后在github的frida下载对应版本架构的server,可以通过adb进入模拟器输入
1 | getprop ro.product.cpu.abi |
查看架构,这里是x86_64的,那么下载如下的frida-server

之后将frida-server放入模拟器的/data/local/tmp中赋予权限运行即可。
然后在windows下frida运行即可hook,有点类似gdb
1 | frida -U -l .\Chap03\2.js com.example.fridapro |

Frida基础知识
HelloWorld
最基础的注入,使用如下js脚本
1 | setTimeout( |
对应注入命令
1 | frida -U -l hello.js android.process.media |
其中-U为USB设备,-l用于指定注入脚本路径,将代码注入到android.process.media这个进程中,即可打印出hello world!。
HOOK基础
对应java代码编写的app如下
1 | package com.example.fridapro; |
即使用fun函数循环打印r0ysue.sum以及算数和,那么就可以使用frida对这个fun函数进行HOOK,如下
1 | function main(){ |
然后在模拟器中运行app,windows中对应的命令也是类似的,指定一下进程名即可
1 | frida -U -l 1.js com.example.fridapro |
使用adb logcat查看即可发现函数参数被修改

主动调用函数
可以使用Frida对APP中没有被调用的函数进行主动调用
例子APP如下
1 | package com.example.fridapro; |
加入了secrect相关函数,其中一个是全局静态的。
对应主动调用的Frida相关的代码如下
1 | function main(){ |
运行之后,查看adb logcat即可看到secrect函数被调用

如果直接运行MainAcitivity.secret();,则会出现如下错误

RPC自动化
可以使用python脚本完成js脚本对进程的注入以及hook。
例子APP如下
1 | package com.example.fridapro; |
在secrect函数中加入了对total属性进行处理的操作。在使用时,如果是static的变量,则直接类加变量名即可获取。如果是类里面的属性变量,则如下代码,也是需要获取类实例才可以的。
1 | function getTotalValue(){ |
那么即可获取到变量值了

下面尝试RPC自动化,在上述js代码中加入CallSecrectFunc来调用secrect函数,如下代码。再加上rpc导出代码,方便后续python复用
1 | function CallSecretFunc(){ |
然后再运行导出即可
TIPS
查看运行的包:
frida-ps -U
当函数被重载之后,使用
Frida进行注入则需要指定参数类型,如下1
2
3MainAcitivity.fun.overload('int', 'int').implementation = function(x,y){
//....
}
Objection逆向
Cha05破解弹窗
Android中常用
Native层HOOK
NDK开发
新建Android studio项目,选择Native C++项目,响应的MainActivity代码如下
1 | package com.example.ndkpro; |
下面进行一些简单的解析:
System.loadLibrary("ndkpro"):代表将这个动态库加载到内存中public native String stringFromJNI():这个函数实际上是一个JNI函数,真实的函数内容是通过C/C++进行实现的,点击C/C++图标可以跳转查看真实代码
1
2
3
4
5
6
7
8
9
10
11
12
extern "C" JNIEXPORT jstring
JNICALL
Java_com_example_ndkpro_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}可以看到是有
C/C++进行实现,并且其命名规则为Java_PackageName_ClassName_MethodName,对应分析一下即可。多出的两个参数
JNIEnv *env和jobject分别表示当前java线程的执行环境和响应函数所在类的对象。此外还有一些对应的数据类型
Java数据类型 JNI数据类型 boolean jboolean byte jbyte void void …. class jclass 除了
void,大多都是在前面加j。
直接使用这个例子编译为APK,解压打开APK会发现多一个lib目录,下面存放四种架构的.so文件,在对应的平台调用不同架构的.so文件

安装APK之后,用objection注入进程,使用命令memory list modules即可查看对应的.so文件是否注入到内存了。

之后可以使用命令memory list export libndkpro.so 导出相关的符号

其他的函数变量都会加上一些乱七八糟的字符,这是因为C++的name mangling名称粉碎机制,如果不需要这个,则需要加上extern "C",而JNI函数就是因为加入这个标识就不会有名称粉碎机制导致函数名变化。

可以使用C++filt对函数名还原

开始HOOK
静态注册JNI函数
上述提到的都是静态注册的JNI函数,有一定的命名方式
修改一下例子,使之循环调用需要Hook的函数
1 | package com.example.ndkpro; |
针对上述例子中的libndkpro.so,使用如下代码进行HOOK
1 | function hook_native(){ |
正常使用frida进行hook即可。
动态注册JNI函数
修改一下模式即可,在native-lib.cpp中更改如下
1 | jstring JNICALL sI3( |
这里即代表对stringFromJNI3的函数调用注册为对sI3的函数调用,在对应的Mainactivity中还是不变,修改成调用stringFromJNI3即可。重新编译运行APP即可看到对应的adb logcat日志

使用这种方式注册的JNI函数的ndkpro.so会找不到stringFromJNI3()字符串相关的函数,这种情况就需要通过先寻找模块基地址,然后寻找偏移地址来获取。
mark:书里面提供的脚本用不了
移动安全升级
第一课、截屏,加壳
破解mmzztt的截屏,首先打开mmzztt,然后用adb即可查看当前apk的一些包信息adb shell dumpsys window | grep mCurrentFocus,注意这里搜索到的com.mmzztt.app是包的名称,用来使用spawn模式重新启动进程,但是实际上启动之后的进程可能并不叫这个名称,可能是其他名称,如果使用attach模式的话,则需要进程名称才行的。

随后需要了解一下截屏机制的相关知识,搜索一下实现Android实现截屏的代码,android应用内代码截屏(获取view快照)和禁止截屏 - Python技术站 (pythonjishu.com)
比较关键的就是如下一段代码

那么就可以搜索一下这个setFlasg的API是否存在调用,直接搜索函数
1 | android hooking search methods setFlags |
会出现一些结果,但是都不是window相关的函数调用

因为提到的Android防截屏知识是获取window对象,然后调用setFlags,那么这里就需要window对象,那么搜索一下window类对象
1 | android hooking search classes window |

会找到一些,但是具体是那个得判断仔细判断一下,这里师傅说盲猜是android.view.Window,那么就搜索一下这个类中的方法是否存在setFlags
1 | android hooking list class_methods android.view.Window |

存在该方法调用,那么就可以hook这个方法,使其不发生作用即可,这里直接用objection好像不太好使,接下来转换为r0tracer进行寻找
r0ysue/r0tracer: 安卓Java层多功能追踪脚本 (github.com)
需要修改一下里面的代码,改为指定函数类即可

然后如下代码
1 | frida -U -f com.mmzztt.app -l r0tracer.js -o mzt.logs |
-f为spawn模式,因为可能在程序运行之后,这个函数已经被调用了,后续不会被调用,那么就需要用这个模式来重启运行,最终在日志中找到如下调用链

依据这个来hook或者其他方式即可,使用如下代码进行hook并打印相关信息
1 | setImmediate(function(){ |
这样frida -U -f com.mmzztt.app -l ./mmzztt.js使用spawn模式启动之后,这样就能截屏了,完成破解

之后使用jadx分析会发现加壳了

那么使用frida一些项目工具进行动态下载dex文件解析,hluwa/frida-dexdump: A frida tool to dump dex in memory to support security engineers analyzing malware. (github.com)
然后在手机上打开对应的应用,在kali中输入frida-dexdump -UF即可

然后搜索一下

然后用jadx打开,通常会失败,显示如下,这是一个jadx的功能,关掉就行

修改一下参数,点击File->Preferences,修改为no

F5重新加载一下即可

第二课、攻防世界基础android
hook逆向基础,拉起activity,广播
攻防世界 (xctf.org.cn)下载APK安装,发现需要输入密码,逆向分析找到Mainactivity,找到button执行逻辑,其实就是调用check.checkPassword

查看一下代码

那么hook一下返回true即可
1 | setImmediate(function(){ |
使用如下命令frida -U -f com.example.test.ctf02 -l ./test.js
然后进入下一关,需要输入然后显示

同样找到该按钮对应代码逻辑,获取数据,然后发送广播sendBroadcast

查看相关的AndroidManifest.xml,可以看到有个过滤,输入android.is.very.fun才能发送广播

再查看广播对应代码,

调用NextContent

onCreate调用到Change

即该change就是以图片方式打开文件timg_2.zip显示出来,找到apk里面的timg_2.zip当作图片打开就能看见flag

此外分析的广播代码也能看到,输入android.is.very.fun也能展示图片

此外由于NextContent是一个单独的Activity,可以直接使用objection拉起,也能看到flag

这里的MainActivity2也能使用这种方式拉起,相当于可以直接绕过关卡。
第三课、六层锁机
第一关 获取函数返回值
打开APK,查看AndroidManifest.xml查找主Activity

可以看到先从LoginActivity开始,查看一下对应代码

即讲username传入a函数进行处理,得到的结果需要与passwd相同。那么我们就可以hook住a函数,传入username,获取函数a的加密结果,然后放入passwd即可,这个可以直接通过objection操作
1 | objection -g com.example.androiddemo explore |
即可获取到

第二关 hook函数返回值
来到BaseFridaActivity,点击按钮调用到onClick,调用到FridaActivity1的onCheck,查看一下代码

放入a函数加密之后需要为R4jSLLLLLLLLLLOrLE7/5B+Z6fsl65yj6BgC6YWz66gO6g2t65Pk6a+P65NK44NNROl0wNOLLLL=,那么hook掉这个函数的返回值即可
1 | function one(){ |
第三关 设置值,函数调用
来到下一关FridaActivity2

那么hook一下onCheck函数,将两个属性置为True即可
1 | function two(){ |
也可以不用hook掉onCheck函数,可以直接设置值的
1 | function two(){ |
也可以调用接口
1 | function two(){ |
关键知识点就是当属性或者函数需要借助于对象时,那么就需要先获取一个对象才能调用或者设置值。静态的使用java.use获取到类就可以直接使用。
第四关 函数名变量名相同
进入到FridaActivity4

同样直接设置一下值即可,需要注意的时直接当变量属性和函数名称一样时,找对应的变量名称在前面需要加上_下划线,如下代码所示
1 | function three(){ |
第五关 java反射,静态内部类
进入FridaAacvitity4

hook掉所有check函数返回true即可,这个我写出来不行,还是按照师傅的用java反射把,但是还是不行,不太会了,估计是哪边有点问题把。
1 | function Activity4(){ |
第六关 hook动态加载类
直接拉起下一关

主要还是在getDynamicDexCheck上

会加载一个类作为DynamicDexCheck

随后调用这个类的check方法来判断

那么就需要hook动态加载这个类,然后hook里面的check方法即可。
1 | function five(){ |
这里的hook不是很懂,先mark
第七关
进入FridaActivity6

hook掉对应类的check函数即可,这里总是不成功,很奇怪
1 | function six(){ |
基础知识
参考:https://www.52pojie.cn/thread-1781093-1-1.html,这是基础知识,文件结构部分。
安卓文件结构
res目录: 存放资源文件,包括icon,xml文件,目录存放资源文件,包括图片,字符串等等,APK的脸蛋由他的layout文件设计
res/layout/: 存放被编译为屏幕布局(或屏幕的一部分)的XML文件
res/values/: 存放可以被编译成很多类型的资源文件
Bool:包含布尔值的 XML 资源,保存在 的 XML 文件:
res/values-small/bools.xml。color:包含颜色值(十六进制颜色)的 XML 资源,保存在 的 XML 文件:
res/values/colors.xml。dimen:包含尺寸值(及度量单位)的 XML 资源,保存在 的 XML 文件:
res/values/dimens.xml。id:提供应用资源和组件的唯一标识符的 XML 资源,保存在 的 XML 文件:
res/values/ids.xml。integer:包含整数值的 XML 资源,保存在 的 XML 文件:
res/values/integers.xml。integers:提供整数数组的 XML 资源,保存在 的 XML 文件:
res/values/integers.xml。array:提供 (可用于可绘制对象数组)的 XML 资源,保存在 的 XML 文件:
res/values/arrays.xml。
AndroidManifest.xml文件: 应用程序配置文件,每个应用都必须定义和包含的,它描述了应用的名字、版本、权限、引用的库文件等信息。
classes.dex文件: 传统 Class 文件是由一个 Java 源码文件生成的 .Class 文件,而 Android 是把所有 Class 文件进行合并优化,然后生成一个最终的 class.dex 文件。它包含 APK 的可执行代码,是分析 Android 软件时最常见的目标。由于dex文件很难看懂,可通过apktool反编译得到.smali文件,smali文件是对Dalvik虚拟机字节码的一种解释(也可以说是翻译),并非一种官方标准语言。通过对smali文件的解读可以获取源码的信息。
resources.arsc文件: 二进制资源文件,包括字符串等,resources.arsc文件只包含资源的索引和映射关系,并不包含实际的资源内容。实际的资源内容存储在res文件夹中,按照资源类型和名称进行组织。当应用程序需要使用资源时,系统会根据resources.arsc文件中的索引信息找到对应的资源文件,并将其加载到内存中。
smali: smali是将Android字节码用可阅读的字符串形式表现出来的一种语言,可以称之为Android字节码的反汇编语言。利用apktool或者Android Killer,反编classes.dex文件,就可以得到以smali为后缀的文件,这些smali文件就是Dalvik的寄存器语言。
asssets目录:存放APK的静态资源文件,比如视频,音频,图片等
lib目录:armeabi-v7a基本通用所有android设备,arm64-v8a只适用于64位的android设备,x86常见用于android模拟器,其目录下的.so文件是c或c++编译的动态链接库文件
META-INF目录:保存应用的签名信息,签名信息可以验证APK文件的完整性,相当于APK的身份证(验证文件是否又被修改)
Kotlin:代表该
APK部分或者全部使用Kotlin进行开发
AndroidManifest.xml
常见属性
AndroidManifest.xml文件是整个应用程序的信息描述文件,定义了应用程序中包含的Activity,Service,Content provider和BroadcastReceiver组件信息。每个应用程序在根目录下必须包含一个AndroidManifest.xml文件,且文件名不能修改。它描述了package中暴露的组件,他们各自的实现类,各种能被处理的数据和启动位置。
| 属性 | 定义 |
|---|---|
| versionCode | 版本号,主要用来更新,例如:12 |
| versionName | 版本名,给用户看的,例如:1.2 |
| package | 包名,例如:com.zj.52pj.demo |
| uses-permission android:name=”” | 应用权限,例如:android.permission.INTERNET 代表网络权限 |
| android:label=”@string/app_name” | 应用名称 |
| android:icon=”@mipmap/ic_launcher” | 应用图标路径 |
| android:debuggable=”true” | 应用是否开启debug权限 |
有时候可以反编译之后,在该文件中找到某些权限,删除不必要的权限获取,这样就安全多了。
标签
- manifest标签
manifest标签是AndroidManifest.xml文件的根标签,它包含了应用程序的基本信息,如包名、版本号、SDK版本、应用程序的名称和图标等等。
- application标签
application标签是应用程序的主要标签,它包含了应用程序的所有组件,如Activity(活动)、Service(服务)、Broadcast Receiver(广播接收器)、Content Provider(内容提供者)等等。在application标签中,也可以设置应用程序的全局属性,如主题、权限等等。
- activity标签
activity标签定义了一个Activity组件,它包含了Activity的基本信息,如Activity的名称、图标、主题、启动模式等等。在activity标签中,还可以定义Activity的布局、Intent过滤器等等。
- service标签
service标签定义了一个Service组件,它包含了Service的基本信息,如Service的名称、图标、启动模式等等。在service标签中,还可以定义Service的Intent过滤器等等。
- receiver标签
receiver标签定义了一个BroadcastReceiver组件,它包含了BroadcastReceiver的基本信息,如BroadcastReceiver的名称、图标、权限等等。在receiver标签中,还可以定义BroadcastReceiver的Intent过滤器等等。
- provider标签
provider标签定义了一个Content Provider组件,它包含了Content Provider的基本信息,如Content Provider的名称、图标、权限等等。在provider标签中,还可以定义Content Provider的URI和Mime Type等等。
- uses-permission标签
uses-permission标签定义了应用程序需要的权限,如访问网络、读取SD卡等等。在应用程序安装时,系统会提示用户授权这些权限。
- uses-feature标签
uses-feature标签定义了应用程序需要的硬件或软件特性,如摄像头、GPS等等。在应用程序安装时,系统会检查设备是否支持这些特性。
安卓四大组件
| 组件 | 描述 |
|---|---|
| Activity(活动) | 在应用中的一个Activity可以用来表示一个界面,意思可以理解为“活动”,即一个活动开始,代表 Activity组件启动,活动结束,代表一个Activity的生命周期结束。一个Android应用必须通过Activity来运行和启动,Activity的生命周期交给系统统一管理。 |
| Service(服务) | Service它可以在后台执行长时间运行操作而没有用户界面的应用组件,不依赖任何用户界面,例如后台播放音乐,后台下载文件等。 |
| Broadcast Receiver(广播接收器) | 一个用于接收广播信息,并做出对应处理的组件。比如我们常见的系统广播:通知时区改变、电量低、用户改变了语言选项等。 |
| Content Provider(内容提供者) | 作为应用程序之间唯一的共享数据的途径,Content Provider主要的功能就是存储并检索数据以及向其他应用程序提供访问数据的接口。Android内置的许多数据都是使用Content Provider形式,供开发者调用的(如视频,音频,图片,通讯录等) |
1.Activity
例子解析
在AndroidManifest.xml中有一些代码可以查看
1 | <!---声明实现应用部分可视化界面的 Activity,必须使用 AndroidManifest 中的 <activity> 元素表示所有 Activity。系统不会识别和运行任何未进行声明的Activity。-----> |
所有界面都是从主要的Activity一步一步启动的,比如启动广告流程:启动Activity->广告Activity->主页Activity。
生命周期
| 函数名称 | 描述 |
|---|---|
| onCreate() | 一个Activity启动后第一个被调用的函数,常用来在此方法中进行Activity的一些初始化操作。例如创建View,绑定数据,注册监听,加载参数等。 |
| onStart() | 当Activity显示在屏幕上时,此方法被调用但此时还无法进行与用户的交互操作。 |
| onResume() | 这个方法在onStart()之后调用,也就是在Activity准备好与用户进行交互的时候调用,此时的Activity一定位于Activity栈顶,处于运行状态。 |
| onPause() | 这个方法是在系统准备去启动或者恢复另外一个Activity的时候调用,通常在这个方法中执行一些释放资源的方法,以及保存一些关键数据。 |
| onStop() | 这个方法是在Activity完全不可见的时候调用的。 |
| onDestroy() | 这个方法在Activity销毁之前调用,之后Activity的状态为销毁状态。 |
| onRestart() | 当Activity从停止stop状态恢进入start状态时调用状态。 |

编译过程
第一步:打包资源文件,生成R.java文件
通过利用aapt资源打包工具,将文件目录中的Resource文件(就是工程中res中的文件)、Assets文件、AndroidManifest.xml文件、Android基础类库(Android.jar文件)进行打包,生成R.java
第二步:aidl生成Java文件
AIDL是Android Interface Definition Language的简称, 是Android跨进程通讯的一种方式。 检索工程里所有的aidl文件,并转换为对应的Java文件
第三步:编译Java文件,生成对应的.class文件
将R.java、aidl生成的Java文件、Java源文件通过JDK携带的Javac编译生成.class文件
第四步:把.class文件转化成Davik VM支持的.dex文件
通过dx工具将.class文件生成为classes.dex
第五步:打包生成未签名的.apk文件
利用apkbuilder工具,将resources.arsc、res目录、AndroidManifest.xml、assets目录、dex文件打包成未签名的apk
第六步:对未签名.apk文件进行签名
使用apksigner为安装包添加签名信息。
第七步:对签名后的.apk文件进行对齐处理
使用zipalign工具对签名包进行内存对齐操作, 即优化安装包的结构。
双开知识
即让手机同时运行或多个相同的软件
| 原理 | 解释 |
|---|---|
| 修改包名 | 让手机系统认为这是2个APP,这样的话就能生成2个数据存储路径,此时的多开就等于你打开了两个互不干扰的APP,这个更改的是类似于com.xxx.xxx这个包名,可以在MT/NP管理器中的安装包提取->提取安装包->定位->点击提取的安装包->功能->APK共存,之后确认即可。但是这个会修改掉包的签名信息,如果开发者进行了签名校验,则会出错。 |
| 修改Framework | 对于有系统修改权限的厂商,可以修改Framework来实现双开的目的,例如:小米自带多开 |
| 通过虚拟化技术实现 | 虚拟Framework层、虚拟文件系统、模拟Android对组件的管理、虚拟应用进程管理 等一整套虚拟技术,将APK复制一份到虚拟空间中运行,例如:平行空间 |
| 以插件机制运行 | 利用反射替换,动态代{过}{滤}理,hook了系统的大部分与system—server进程通讯的函数,以此作为“欺上瞒下”的目的,欺骗系统“以为”只有一个apk在运行,瞒过插件让其“认为”自己已经安装。例如:VirtualApp |
不是很懂,后门应该可以学到
汉化
基本上字符串都是反编译结果的资源文件里面,在arsc里,建议一键汉化,然后再润色。少量没汉化到的字符串参考视频中的方法定位去逐个汉化。
MT管理器->提取安装包->点击安装包->查看->右上角的三点展开->搜索(高级搜索)->文件中包含内容搜索
XML形式
一般搜索对应的英文即可,大多保存在xxx.xml文件中,然后修改即可,如下的Hello,修改成中文即可

记得勾选签名

Arsc形式
在不知道什么语言的时候,可以使用开发者助手来获取文本。
打开开发者助手,然后打开对应的应用中的未知语言所在界面,点击开发者助手小图标,找到界面资源分析,点击开始

然后关闭进度条的界面,点击对应的文本即可

得到对应的文件文本,随后即可进行搜索

找到之后,选择翻译模式打开,进入default默认栏,然后查找修改即可。
Dex形式
搜索找到文件,使用Dex ++编辑器打开,选择字符串模式进行搜索,然后修改即可。
smali知识
可以用语法查询工具,有个apk可以安装,在安卓逆向这档事的课程三中有

关键字
在smali语法中的关键字相关含义功能
| 名称 | 注释 |
|---|---|
| .class | 类名 |
| .super | 父类名,继承的上级类名名称 |
| .source | 源名 |
| .field | 变量 |
| .method | 方法名 |
| .register | 寄存器 |
| .end method | 方法名的结束 |
| public | 公有 |
| protected | 半公开,只有同一家人才能用 |
| private | 私有,只能自己使用 |
| .parameter | 方法参数 |
| .prologue | 方法开始 |
| .line xxx | 位于第xxx行 |
数据类型定义
smali中一些类型和java的对应关系
| smali类型 | java类型 | 注释 |
|---|---|---|
| V | void | 无返回值 |
| Z | boolean | 布尔值类型,返回0或1 |
| B | byte | 字节类型,返回字节 |
| S | short | 短整数类型,返回数字 |
| C | char | 字符类型,返回字符 |
| I | int | 整数类型,返回数字 |
| J | long (64位 需要2个寄存器存储) | 长整数类型,返回数字 |
| F | float | 单浮点类型,返回数字 |
| D | double (64位 需要2个寄存器存储) | 双浮点类型,返回数字 |
| string | String | 文本类型,返回字符串 |
| Lxxx/xxx/xxx | object | 对象类型,返回对象 |
常用指令
| 关键字 | 注释 | 例子 | 解释 |
|---|---|---|---|
| const | 重写整数属性,真假属性内容,只能是数字类型 | ||
| const-string | 重写字符串内容 | ||
| const-wide | 重写长整数类型,多用于修改到期时间。 | ||
| return | 返回指令 | ||
| if-eq | 全称equal(a=b),比较寄存器ab内容,相同则跳 | ||
| if-ne | 全称not equal(a!=b),ab内容不相同则跳 | ||
| if-eqz | 全称equal zero(a=0),z即是0的标记,a等于0则跳 | ||
| if-nez | 全称not equal zero(a!=0),a不等于0则跳 | ||
| if-ge | 全称greater equal(a>=b),a大于或等于则跳 | ||
| if-le | 全称little equal(a<=b),a小于或等于则跳 | ||
| goto | 强制跳到指定位置 | ||
| switch | 分支跳转,一般会有多个分支线,并根据指令跳转到适当位置 | .line 54 packed-switch p1, :pswitch_data_0 .line 54 :pswitch_data_0 .packed-switch 0x1 :pswitch_0 :pswitch_1 .end packed-switch |
首先会有一个结构packed-switch p1,:pswitch_data_0,代表跳转到pswitch_data_0,然后在pswitch_data_0中具体定义相关的switch结构,变量为p1 |
| iget | 获取寄存器数据 |
寄存器
在smali里的所有操作都必须经过寄存器来进行:本地寄存器用v开头数字结尾的符号来表示,如v0、 v1、v2。 参数寄存器则使用p开头数字结尾的符号来表示,如p0、p1、p2。特别注意的是,p0不一定是函数中的第一个参数,在非static函数中,p0代指“this",p1表示函数的第一个 参数,p2代表函数中的第二个参数。而在static函数中p0才对应第一个参数(因为Java的static方法中没有this方法)
例子解析
1 | //一个私有、静态、不可变的方法 方法名 |
其中unicode可以通过cyberchef进行确定

还有一些指令
- if-eqz vA, vB, :cond_” 如果vA等于vB则跳转到:
cond_ - if-nez vA, vB, :cond_” 如果vA不等于vB则跳转到:
cond_
代码混淆
加固手法
第一阶段:DEX整体加固
即将
DEX整体加密后动态加载。加载时DEX加密文件整体解密后再用DexClassLoader或者其他类来加载解密后的文件。第二阶段:代码抽取保护
函数真正的运行代码并不与
DEX整体结构数据存储在一起,真正的加密核心原理为利用私有函数,通过对自身进程的Hook来拦截函数被调用的路径,在抽取的函数被真实调用之前,将无意义的代码数据填充到对应的代码区中,比如nop。对抗工具有,
DexHunter第三阶段:VMP与Dex2C
将所有的
JAVA代码都变成最终的Native层代码(汇编)。
常用工具
ProGuard:更改类名,函数名,变量名,在Android studio中的根目录下面的App/build.gradle文件中,将buildTypes层级的isMinifyEnabled改为True即可
DexGuard:除了ProGuard提供的功能外,还有字符串加密,花指令以及运行库防护等等。
弹窗
Android中常用的弹窗类主要有三种,分别是android.App.Dialog、android.App.AlertDialog和android.widget.PopupWindow,这些弹窗在打开APP运行起来之后,在内存中一般都有实例运行,那么找到这些实例即可。
HOOK抓包
基础知识
主要是网络框架的知识,现在的大多网络通信框架有如下几种
HttpURLConnect:原生的Android网络通信框架okhttp、Volley、WebSocket、XMPP等:
特定框架抓包
一般情况Android的收发包相关框架为okhttp、okhttp3、HTTPURLConnection三种框架,那么我们需要做的就是定位这三个框架调用到的类。
1.删除~/.objection目录,即删除旧的objection.log确保后面运行得到的日志只有本次APP的内容。
2.Objection附加进程APP,输入命令android hooking list classes获取加载的所有类,退出objection
3.从objection.log中过滤出所有网络框架类okhttp、okhttp3、HTTPURLConnection
1 | cat ./objection.log | grep -i HttpURLConnection > htpc.txt |
4.使用vscode补全导出的文件,Alt+Shift+鼠标向下拖动,可以选定所有行进行补全,使之成为一个objection命令

5.使用objection的-c选项对文件中所有命令进行执行

6.所有类都hook上之后,在APP上尝试登录,即可看到一堆执行的函数

7.任意选择上述被调用的函数,比如com.android.okhttp.internal.http.StreamAllocation.release,命令如下
1 | objection -g com.cz.babySister explore |
随后在app中尝试登录,出现如下调用栈

经过经验分析,可以得到最终的发包函数为com.cz.babySister.c.a.a
签名校验
APK的签名作用通常用来证明APK所有者以及允许校验APK的正确性。
目前的Android支持如下四种签名方案
v1方案:基于JAR签名。这个签名机制主要就在
META-INF目录下的三个文件,MANIFEST.MF,ANDROID.SF,ANDROID.RSA,他们都是V1签名的产物。MANIFEST.MF:摘要文件。签名时,程序遍历Apk包中的所有文件(entry),对非文件夹非签名文件的文件,逐个用SHA1(安全哈希算法)生成摘要信息,再用Base64进行编码生成该摘要文件。如果改变了APK包中内容,那么这个MANIFEST.MF摘要文件在签名时就会发生改变。ANDROID.SF:签名文件。签名时对MANIFEST.MF摘要文件使用SHA1-RSA,用开发者的私钥进行加密(签名)生成该ANDROID.SF文件。在安装时或者打开应用某个功能时进行签名校验,用开发者的公钥对该签名文件ANDROID.SF进行解密,得到解密后的摘要文件MANIFEST.MF_install,使之与摘要文件MANIFEST.MF进行比对,如果内容相符,则代表该APK在签名时到安装这个过程中没有被修改,如果不相符,则被修改了,程序会无法安装,或者安装后闪退。ANDROID.RSA:密钥文件,保存了公钥以及所采用的加密算法等信息,上述验证过程中用到的公钥即在这个文件中,在签名时会一同导入到该文件。
可以发现上述签名过程能保证的是在开发者签名之后的
APK文件没被改变,但是如果我们将APK文件逆向反编译进行修改,然后用自己的私钥签名,再把自己的公钥放入ANDROID.RSA密钥文件中,在签名到签名校验这一个过程中APK文件并没有被改变,还是可以安装正常使用,也就绕过了V1方案。v2方案:APK签名方案v2(在Android 7.0中引入)v3方案:APK签名方案v3(在Android 9中引入)v4方案:APK签名方案v4(在Android 11中引入)
上述几种签名方案详细解读可以参考:APK 签名方案 v2 | Android 开源项目 | Android Open Source Project
那么有签名大多都有签名校验,签名校验失败后通常有如下几种形式来讲应用停止kill/killProcess、system.exit、finish
TIPS
如果存在针对okhttp框架的混淆,则可以使用siyujie/OkHttpLogger-Frida: Frida 实现拦截okhttp的脚本 (github.com)来进行对抗
刷机
Pixel4
首先进入pixel4 kernelsu的github,最新的即可

查看build number

进入https://developers.google.com/android/images#redfin,下载对应版本的刷机包,注意要是`pixel4`才行,点击`link`下载下来

pixel4关机之后按住音量减和电源键进入fastboot状态,会有红色感叹号的fastboot,,运行刷机包中的./flash-all.sh即可

刷完之后,github上搜索对应机型的kernelsu

release上下载img文件即可

再关机,按住音量减和电源键进入fastboot状态,使用如下命令再刷入flash即可
1 | fastboot flash boot pixel4xl_android13_4.14.276_v068.img |
刷完之后,重启即可。
然后再github上找kernelsu,进入release,下载对应版本的apk进行安装即可

安装完成之后,即可使用scrcpy远程桌面,即可通过kernelSu进行管理


红米note8Pro
https://zhuanlan.zhihu.com/p/360655776
解锁BLM
查看教程,用的是官解
刷TWRP
用的是这个
小米全机型TWRP一键刷机工具:
百度网盘下载:https://pan.baidu.com/s/15lHG5eibrZ3nsS8CM_JVqg 提取码:pc5n
参考:http://www.romleyuan.com/lec/read?id=201
然后手机进入到Fastboot,运行刷机工具里面的.bat即可,就能进入到TWRP界面。
然后安装Magisk即可,需要后缀为.zip才行。
1 | adb shell twrp install /tmp/magisk.zip |
重启正常安装Magisk即可
壳
查壳
CTF题目
工具集合
基础工具
| 工具名称 | 描述 | 链接 |
|---|---|---|
| Magisk | 获取root,管理root权限,管理模块 | Magisk |
| LSPosed | 管理XPosed框架 | LSPosed |
| LSPatch | 用来将XPosed框架模块打包到APK | LSPatch |
| MT管理器 | 打包,重新签名,修改APK神器 | MT管理器 |
| 算法助手 | 利用XPosed框架提供各种hook功能 | 算法助手 |
| 开发助手 | 主要用来查看布局情况 | 手机商城就能下载 |
| XappDebug | 用来hook对应APP使其可以调试 | XappDebug |
调试工具
| 工具名称 | 描述 | 链接 |
|---|---|---|
| JEB | APK分析调试 | JEB5.5 |
Trace工具
| 工具名称 | 描述 | 链接 |
|---|---|---|
| jnitrace | 老牌,经典,信息全,携带方便 | jnitrace |
| jnitrace-engine | 基于jnitrace,可定制化 | jnitrace-engine |
| jtrace | 定制方便,信息全面,直接在_agent.js或者_agent_stable.js 里面加自己的逻辑就行 | jtrace |
| hook_art.js | 可提供jni trace,可以灵活的增加你需要hook的函数 | hook_art.js |
| JNI-Frida-Hook | 函数名已定义,方便定位 | JNI-Frida-Hook |
| findhash | ida插件,可用于检测加解密函数,也可作为Native Trace库 | findhash |
| Stalker | frida官方提供的代码跟踪引擎,可以在Native层方法级别,块级别,指令级别实现代码修改,代码跟踪 | Stalker |
| sktrace | 类似 ida 指令 trace 功能 | sktrace |
| frida-qbdi-tracer | 速度比frida stalker快,免补环境 | frida-qbdi-tracer工具名称 |
frida-trace
这个好像没有什么用,貌似是把所有的C相关函数都trace了,不太懂,也可能是用法不太对
官方文档
frida-trace 可以一次性监控一堆函数地址。还能打印出比较漂亮的树状图,不仅可以显示调用流程,还能显示调用层次。并且贴心的把不同线程调用结果用不同的颜色区分开了。
大佬整理的文档:
frida-trace
-i/-a: 跟踪 C 函数或 so 库中的函数。
PS:-a 包含模块+偏移跟踪,一般用于追踪未导出函数,例子:-a “lib52pojie.so!0x4793c”
包含/排除模块或函数:
-I: 包含指定模块。-X: 排除指定模块。
Java 方法跟踪:
-j JAVA_METHOD: 包含 Java 方法。-J JAVA_METHOD: 排除 Java 方法。
附加方式:
-f:通过 spwan 方式启动-F:通过 attach 方式附加当前进程
日志输出:
-o:日志输出到文件
示例:
frida为15.2.2
1 | frida-trace -U -F -I "lib52pojie.so" -i "Java_" #附加当前进程并追踪lib52pojie.so里的所有Java_开头的jni导出函数 |

jnitrace
版本
1
frida==16.1.4 ,python==3.9.9,jnitrace==3.3.0
l libnative-lib.so- 用于指定要跟踪的库-m <spawn|attach>- 用于指定要使用的 Frida 附加机制-i <regex>- 用于指定应跟踪的方法名称,例如,-i Get -i RegisterNatives将仅包含名称中包含 Get 或 RegisterNatives 的 JNI 方法-e <regex>- 用于指定跟踪中应忽略的方法名称,例如,-e ^Find -e GetEnv将从结果中排除所有以 Find 开头或包含 GetEnv 的 JNI 方法名称-I <string>- 用于指定应跟踪的库的导出-E <string>用于指定不应跟踪的库的导出-o path/output.json- 用于指定jnitrace存储所有跟踪数据的输出路径
1 | jnitrace -m attach -l lib52pojie.so wuaipojie -o trace.json |

有时候出错,多点两下

sktrace
跑的比较慢,整体的trace
版本:
1
frida==16.1.4 ,python==3.9.9
- 类似 ida 指令 trace 功能
- 统计寄存器变化,辅助分析,并且可能会有字符串产生
1 | python sktrace.py -m attach -l lib52pojie.so -i 0x103B4 wuaipojie > test.log |

可以搜索字符串

抓包
需要用到kali中的charles软件,使用charles导出证书为123.pem

然后push进入证书,用户安装证书。之后需要使用kernelSu安装Move_certificatesmovecert,用来把用户证书放入到系统中,然后重启一下。

随后需要安装postern设置vpn,配置一下代理和规则即可


现在就可以开始抓包了,使用postern打开vpn,然后在kali打开charles即可

最终实现抓包

TIPS
1.AndroidKill修改签名
Androidkill原始的签名只能是V1,但是现在使用v1签名在安卓11以上无法安装,所以更改一下签名脚本AndroidRev\Tool\AndroidKiller\bin\apktool\signer.bat
更改为:
1 | @echo off |
这个apksigner.jar用哪个都行,需要放入AndroidRev\Tool\AndroidKiller\bin\apktool\中
同时需要用如下命令生成签名密钥my-release-key.keystore,密码都设置为123456即可
1 | keytool -genkey -v -keystore my-release-key.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000 |
将生成的签名密钥my-release-key.keystore放入AndroidRev\Tool\AndroidKiller\bin\apktool\即可。
注意更新一下java
2.4字节对齐问题
1 | PS D:\Work\AndroidRev\Tool\AndroidKiller\projects\jiaochen\Bin> adb install .\jiaochen_killer.apk |
这时候就是4字节对问题
参考:https://blog.csdn.net/jeephao/article/details/117673542
那么就需要在签名之前用zipalign对齐一下,将zipalign.exe放入AndroidRev\Tool\AndroidKiller\bin\apktool\中,修改一下上面的脚本
1 | @echo off |
3.smali代码参数问题
特别注意的是,p0不一定是函数中的第一个参数,在非static函数中,p0代指“this”,p1表示函数的第一个 参数,p2代表函数中的第二个参数。而在static函数中p0才对应第一个参数(因为Java的static方法中没有this方法
4.导入C语言头结构问题
在JNI分析时,有时候.so文件没有保存符号,就需要导入jni.h头文件,然后就自己选择相关结构进行解析:
File->Load file->Parse C header file

选择jni.h解析之后,在变量右键,选择Convert to struct*,即可看到导入的结构

如下所示

在逆向时都可以的
5.NDK的so文件调试
导入IDA的android_server用root运行

adb运行端口转发

IDA附加调试即可

6.JNI注册函数
静态注册函数
在JAVA层如下,有static修饰的

在IDA中一般在Exports导出函数中可以查看到

动态注册函数
- 在JAVA层如下,动态注册函数

- 一般在IDA的.data.rel.ro.local段,这个sub_1174即为注册函数checkport的地址

并且第一个参数均为__JNIEnv*,可以导入jni.h进行修改
7.反调试
常见的反调试有:[原创]【SO壳】17种安卓native反调试收集-Android安全-看雪-安全社区|安全招聘|kanxue.com
8.IDA调试问题
在连接so进行调试时
1 | jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700 (jdb挂起) |
这个可能导致
1 | 2.java.io.IOException: handshake failed - connection prematurally closed |
大多是手机问题,或者存在反调试,选择低版本的手机
9.永久调试权限问题
这个问题出现在JEB4上面,换成JEB5可以了
当用MagiskHide Props Config获取永久调试权限时

再使用JEB调试,记得还是需要修改一下AndroidManifest.xml的debuggable,不然JEB可能还是无法调试,估计是JEB分析APK时,也会检查APK中的AndroidManifest.xml的debuggabel选项,如果没设置或者不为True,那么仍然不给调试,这个可能时JEB的关系,和手机APP没什么关系。
《安卓逆向这档事》五、1000-7=?&动态调试&Log插桩 - 『移动安全区』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn
10.算法助手与Android13
有时候算法助手会有这个要求

但是点进去又无法成功,使用此文件夹还是不行

需要在Android/data/目录下新建包名文件夹,使用这个文件夹即可,此时算法助手就能自动定位到了

参考
《教我兄弟学Android逆向系列课程+附件导航帖》 - 『移动安全区』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn
《关于我在吾爱破解论坛学安卓逆向这档事》预告 - 『水漫金山』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn