Android逆向

教我兄弟学Android逆向

03 破解第一个Android游戏

切水果大作战

方法一

《新人贴》初次尝试破解内购小游戏:切水果大作战 - 『移动安全区』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

正常打开搜索失败字符串即可

image-20230918145730271

定位到找到对应的三个函数payResultSuccesspayResultFalsepayResultCancel

image-20230918145809775

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

image-20230918145918972

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

image-20230918145938340

然后在AndroidKillsmali代码窗口中将payResultSuccess的代码原封不动复制到payResultFalsepayResultCancel即可,最后编译即可。

image-20230918150342671

方法二

可以通过修改跳转来使得任意点击均可跳转到payResultSuccess

全局搜索payResultFalse,在MiGuSdkPay$1.smali中有对应的switch跳转语句

image-20230918153302711

这里的

1
2
.line 54
packed-switch p1, :pswitch_data_0

即为声明switch语句,跳转至pswitch_data_0,在这个结构语句里面具体进行跳转

1
2
3
4
5
6
.line 54
:pswitch_data_0
.packed-switch 0x1
:pswitch_0
:pswitch_1
.end packed-switch

那么就是值为0则跳转pswitch_0,为1则跳转pswitch_1

查看对应的java源代码

image-20230918152641129

可以看到对应的一些跳转,依据选择不同,那么就在smali代码中修改对应的跳转即可,即将pswitch_0修改为pswitch_1即可。

08 IDA爆破签名验证

主要获取到可以导入jni.h的知识,在TIPS

09 IDA动态破解登陆验证

主要获取到IDA动态调试.so的知识,在TIPS

10 静态分析反调试apk

init_array和JNI_OnLoad会在so加载的时候就开始执行,所以程序也有可能会在这里开启线程进行反调试。

反调试点

  • checkport

    在MainActivity中调用了

    image-20240117150345567

    如果存在结果,即0x5d8a=23946端口被占用,那么就会exit,否则就挑战成功。

    可以通过android_server -p23947设置端口来绕过

  • init_array

    在.so文件加载时就会被运行,比JNI_OnLoad早

    image-20240117150857630

    对应函数

    image-20240117151012814

    跳转函数

    image-20240117151307128

    即fget按行读取,总共6次遍历,获取/proc/xx/status的第六行,即TracePid,查看里面的数据是否大于0,大于0则代表程序正在被调试,即退出。

    image-20240117151614732

    可以刷机改内核,使得TracerPid永远为0

    逆向修改内核,绕过TracerPID反调试 - 『移动安全区』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

  • JNI_OnLoad函数

    在执行System.loadLibrary("xxx.so")时被调用

    image-20240117152056226

    进入SearchObjProcess函数

    image-20240117152231674

    查看是否有这些进程,有就退出

    可以将android_server修改为其他名称

11 动态调试init_array

没成功,寄

不过调试最开始的需要在手机中这样运行才行,在Frida中好像也讲过

1
am start -D -n demo2.jni.com.myapplication/.MainActivity

安卓逆向这档事

课程二、初识APK文件结构、双开、汉化、基础修改

参考基础知识中的汉化知识块。

汉化未知语言

同时MT管理器还有一键翻译的功能,下载对应的翻译插件即可。

课后作业

汉化

可以先进行通用搜索

image-20240125092914594

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

image-20240125093020630

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

image-20240125093006776

点击[DEFAULT]

image-20240125093112008

找到对应字符串即可修改

image-20240125093155655

应用双开

MT管理器就可以

替换图片

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

image-20240125100604466

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

image-20240125100647160

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

image-20240125100728804

随后到resources.arsc中的编辑器

image-20240125101214945

通过ID定位资源

image-20240125101229576

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

image-20240125102954025

课程三、初识smali、VIP终结者

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

image-20230920173517991

image-20230920173719749

找到修改即可

image-20230920173732547

课程四、恭喜获得广告&弹窗静默卡

获取Activity记录

可以用MT管理器来获取一个APK启动的Activity的记录,在MT主界面左上角点击三横线图标展开,启动Activity记录服务

image-20230921085806851

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

image-20230921090029390

去除广告

对于广告来说,去除的方法通常有如下几种

  • 第一种:修改加载时间,把广告的显示时间修改为0秒即可

    获取广告Activity的名称,反编译classes.dex进行类名搜索

    image-20230921091645061

    然后找到类的smali路径进行搜索

    image-20230921100539630

    再进行代码搜索

    image-20230921100626230

    进入到如下的AdActivity类中代码进行分析

    image-20230921104708223

    即在主函数onCreate中调用了广告代码loadAd,随后启动广告,持续3000ms

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    protected 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中有调用

    image-20230921110629477

    进入查看定位关键代码

    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
    private 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即可。

    image-20230921111702702

    然后保存重新安装即可。

弹窗修改

最主要的是定位弹窗在哪。

针对更新弹窗而言,一般都是在代码逻辑里判断新版本和当前版本是否版本号一致,那么可以直接修改XML中的versiocode为最新版本即可

image-20230921112413531

还有的可以用算法助手工具进行直接Hook,有弹窗定位,关键词屏蔽功能等,这样就可以把某些返回键禁止的弹窗给hook了。

  • 修改dex代码

    使用开发助手的日志功能对弹窗进行定位,找到对应的代码,注释掉show方法修改即可。

    image-20230921142007647

  • 抓包修改响应体

    使用开发助手->布局查看功能找到横幅广告图片的view Id在哪,修改高度宽度即可

    image-20230921141310674

    image-20230921142647394

    随后进行XML搜索即可,修改长宽。

    image-20230921165853598

    另外也可以加入一段代码

    1
    android:visibility="gone"

课后作业

使用算法助手的弹窗定位功能,定位到关键函数,这个函数是被混淆过的函数。

image-20240125145728792

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

image-20240125145840175

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

image-20240125150024308

都把那个show给注释掉即可,或者查找调用点,在调用点处注释即可

或者使用关键字”弹窗”来定位代码也是一样的。

课程五、1000-7=?&动态调试&Log插桩

动态调试

  • 首先需要将AndroidManifest.xml中加入调试权限

    image-20230928101038713

    然后重新安装,再把安装包放入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 表示将应用设置为可调试模式

    这个类名为主类名才行。

    输入命令后,在手机中会显示如下弹窗

    image-20230928101325955

  • 之后点击JEB上的调试即可

    image-20230928101341269

其他的调试方法有点麻烦,不研究了。

Log插桩

  • 将插桩的.dex文件添加到apk中,长按出现添加按钮

image-20230928102311161

image-20230928102327640

image-20230928102342731

  • 随后在需要获取的寄存器值的位置,加入如下代码

    1
    invoke-static {对应寄存器}, Lcom/mtools/LogUtils;->v(Ljava/lang/Object;)V

    如下图所示

    image-20230928103457975

    获取v0的值

课后作业

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

image-20240125153055436

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

image-20240125153141447

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

image-20240125154047803

动态调试的话,也还是有点麻烦,拉到把。

课程六、校验的N次方-签名校验对抗、PM代{过}{滤}理、IO重定向

检测校验

签名校验

通常就是校验包的hash值是否被修改

  • 普通签名校验

    系统将应用的签名信息封装在 PackageInfo 中,调用 PackageManager 的 getPackageInfo(String packageName, int flags) 即可获取指定包名的签名信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    private 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
    2
    ZipEntry 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"

对抗手段

模拟器检测对抗 (notion.site)

签名定位

  • 一般通过算法助手定位闪退的日志

签名校验对抗

  • 一键过签名工具:

    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中的内容,将签名信息和包名修改为指定安装包信息

    签名信息为如下原始数据内容

    image-20240125165748164

  • 然后将两个smali文件放到一个文件夹,使用命令

    1
    java -jar smali.jar assemble -o HookPMS.dex .\zhengji\Hook\

    生成得到HookPMS.dex

  • 随后将HookPMS.dex导入安装包,重新命名classes2.dex,重新打包

    image-20240125170224796

  • 随后搜索attachBaseContext方法,在MainApplication中

    image-20240125170348573

    加入如下代码

    1
    invoke-static {p1}, Lzhengji/Hook/ServiceManagerWraper;->hookPMS(Landroid/content/Context;)V

    注意要和HookPMS.dex中的路径一致

  • Pixel4不行,用模拟器可以跑通

    image-20240125173039659

IO重定向

功能

  • 让文件只读不可写
  • 禁止访问文件
  • 路径替换

用处

  • 过签名校验,当校验时让它读取原包
  • 风控对抗,某些APP会记录启动次数
  • 过Root检测、Xposed检测

原理

hook了底层打开文件的一些路径

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
using namespace std;  
string packname;
string origpath;
string fakepath;

int (*orig_open)(const char *pathname, int flags, ...);
int (*orig_openat)(int,const char *pathname, int flags, ...);
FILE *(*orig_fopen)(const char *filename, const char *mode);
static long (*orig_syscall)(long number, ...);
int (*orig__NR_openat)(int,const char *pathname, int flags, ...);

void* (*orig_dlopen_CI)(const char *filename, int flag);
void* (*orig_dlopen_CIV)(const char *filename, int flag, const void *extinfo);
void* (*orig_dlopen_CIVV)(const char *name, int flags, const void *extinfo, void *caller_addr);

static inline bool needs_mode(int flags) {
return ((flags & O_CREAT) == O_CREAT) || ((flags & O_TMPFILE) == O_TMPFILE);
}
bool startsWith(string str, string sub){
return str.find(sub)==0;
}

bool endsWith(string s,string sub){
return s.rfind(sub)==(s.length()-sub.length());
}
bool isOrigAPK(string path){

if(path==origpath){
return true;
}
return false;
}
//该函数的功能是在打开一个文件时进行拦截,并在满足特定条件时将文件路径替换为另一个路径

//fake_open 函数有三个参数:
//pathname:一个字符串,表示要打开的文件的路径。
//flags:一个整数,表示打开文件的方式,例如只读、只写、读写等。
//mode(可选参数):一个整数,表示打开文件时应用的权限模式。
int fake_open(const char *pathname, int flags, ...) {
mode_t mode = 0;
if (needs_mode(flags)) {
va_list args;
va_start(args, flags);
mode = static_cast<mode_t>(va_arg(args, int));
va_end(args);
}
//LOGI("open, path: %s, flags: %d, mode: %d",pathname, flags ,mode);
//这里被写死了,只能在这个路径下放入原包
string cpp_path= pathname;
if(isOrigAPK(cpp_path)){
LOGI("libc_open, redirect: %s, --->: %s",pathname, fakepath.data());
return orig_open("/data/user/0/com.zj.wuaipojie/files/base.apk", flags, mode);
}
return orig_open(pathname, flags, mode);

}

//该函数的功能是在打开一个文件时进行拦截,并在满足特定条件时将文件路径替换为另一个路径

//fake_openat 函数有四个参数:
//fd:一个整数,表示要打开的文件的文件描述符。
//pathname:一个字符串,表示要打开的文件的路径。
//flags:一个整数,表示打开文件的方式,例如只读、只写、读写等。
//mode(可选参数):一个整数,表示打开文件时应用的权限模式。
//openat 函数的作用类似于 open 函数,但是它使用文件描述符来指定文件路径,而不是使用文件路径本身。这样,就可以在打开文件时使用相对路径,而不必提供完整的文件路径。
//例如,如果要打开相对于当前目录的文件,可以使用 openat 函数,而不是 open 函数,因为 open 函数只能使用绝对路径。
//
int fake_openat(int fd, const char *pathname, int flags, ...) {
mode_t mode = 0;
if (needs_mode(flags)) {
va_list args;
va_start(args, flags);
mode = static_cast<mode_t>(va_arg(args, int));
va_end(args);
}
LOGI("openat, fd: %d, path: %s, flags: %d, mode: %d",fd ,pathname, flags ,mode);
string cpp_path= pathname;
if(isOrigAPK(cpp_path)){
LOGI("libc_openat, redirect: %s, --->: %s",pathname, fakepath.data());
return orig_openat(fd,fakepath.data(), flags, mode);
}
return orig_openat(fd,pathname, flags, mode);

}
FILE *fake_fopen(const char *filename, const char *mode) {

string cpp_path= filename;
if(isOrigAPK(cpp_path)){
return orig_fopen(fakepath.data(), mode);
}
return orig_fopen(filename, mode);
}
//该函数的功能是在执行系统调用时进行拦截,并在满足特定条件时修改系统调用的参数。
//syscall 函数是一个系统调用,是程序访问内核功能的方法之一。使用 syscall 函数可以调用大量的系统调用,它们用于实现操作系统的各种功能,例如打开文件、创建进程、分配内存等。
//
static long fake_syscall(long number, ...) {
void *arg[7];
va_list list;

va_start(list, number);
for (int i = 0; i < 7; ++i) {
arg[i] = va_arg(list, void *);
}
va_end(list);
if (number == __NR_openat){
const char *cpp_path = static_cast<const char *>(arg[1]);
LOGI("syscall __NR_openat, fd: %d, path: %s, flags: %d, mode: %d",arg[0] ,arg[1], arg[2], arg[3]);
if (isOrigAPK(cpp_path)){
LOGI("syscall __NR_openat, redirect: %s, --->: %s",arg[1], fakepath.data());
return orig_syscall(number,arg[0], fakepath.data() ,arg[2],arg[3]);
}
}
return orig_syscall(number, arg[0], arg[1], arg[2], arg[3], arg[4], arg[5], arg[6]);

}

//函数的功能是获取当前应用的包名、APK 文件路径以及库文件路径,并将这些信息保存在全局变量中
//函数调用 GetObjectClass 和 GetMethodID 函数来获取 context 对象的类型以及 getPackageName 方法的 ID。然后,函数调用 CallObjectMethod 函数来调用 getPackageName 方法,获取当前应用的包名。最后,函数使用 GetStringUTFChars 函数将包名转换为 C 字符串,并将包名保存在 packname 全局变量中
//接着,函数使用 fakepath 全局变量保存了 /data/user/0/<packname>/files/base.apk 这样的路径,其中 <packname> 是当前应用的包名。
//然后,函数再次调用 GetObjectClass 和 GetMethodID 函数来获取 context 对象的类型以及 getApplicationInfo 方法的 ID。然后,函数调用 CallObjectMethod 函数来调用 getApplicationInfo 方法,获取当前应用的 ApplicationInfo 对象。
//它先调用 GetObjectClass 函数获取 ApplicationInfo 对象的类型,然后调用 GetFieldID 函数获取 sourceDir 字段的 ID。接着,函数使用 GetObjectField 函数获取 sourceDir 字段的值,并使用 GetStringUTFChars 函数将其转换为 C 字符串。最后,函数将 C 字符串保存在 origpath 全局变量中,表示当前应用的 APK 文件路径。
//最后,函数使用 GetFieldID 和 GetObjectField 函数获取 nativeLibraryDir 字段的值,并使用 GetStringUTFChars 函数将其转换为 C 字符串。函数最后调用 LOGI 函数打印库文件路径,但是并没有将其保存在全局变量中。

extern "C" JNIEXPORT void JNICALL
Java_com_zj_wuaipojie_util_SecurityUtil_hook(JNIEnv *env, jclass clazz, jobject context) {
jclass conext_class = env->GetObjectClass(context);
jmethodID methodId_pack = env->GetMethodID(conext_class, "getPackageName",
"()Ljava/lang/String;");
auto packname_js = reinterpret_cast<jstring>(env->CallObjectMethod(context, methodId_pack));
const char *pn = env->GetStringUTFChars(packname_js, 0);
packname = string(pn);

env->ReleaseStringUTFChars(packname_js, pn);
//LOGI("packname: %s", packname.data());
fakepath= "/data/user/0/"+ packname +"/files/base.apk";

jclass conext_class2 = env->GetObjectClass(context);
jmethodID methodId_pack2 = env->GetMethodID(conext_class2,"getApplicationInfo","()Landroid/content/pm/ApplicationInfo;");
jobject application_info = env->CallObjectMethod(context,methodId_pack2);
jclass pm_clazz = env->GetObjectClass(application_info);

jfieldID package_info_id = env->GetFieldID(pm_clazz,"sourceDir","Ljava/lang/String;");
auto sourceDir_js = reinterpret_cast<jstring>(env->GetObjectField(application_info,package_info_id));
const char *sourceDir = env->GetStringUTFChars(sourceDir_js, 0);
origpath = string(sourceDir);
LOGI("sourceDir: %s", sourceDir);

jfieldID package_info_id2 = env->GetFieldID(pm_clazz,"nativeLibraryDir","Ljava/lang/String;");
auto nativeLibraryDir_js = reinterpret_cast<jstring>(env->GetObjectField(application_info,package_info_id2));
const char *nativeLibraryDir = env->GetStringUTFChars(nativeLibraryDir_js, 0);
LOGI("nativeLibraryDir: %s", nativeLibraryDir);
//LOGI("%s", "Start Hook");

//启动hook
void *handle = dlopen("libc.so",RTLD_NOW);
auto pagesize = sysconf(_SC_PAGE_SIZE);
auto addr = ((uintptr_t)dlsym(handle,"open") & (-pagesize));
auto addr2 = ((uintptr_t)dlsym(handle,"openat") & (-pagesize));
auto addr3 = ((uintptr_t)fopen) & (-pagesize);
auto addr4 = ((uintptr_t)syscall) & (-pagesize);

//解除部分机型open被保护
mprotect((void*)addr, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC);
mprotect((void*)addr2, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC);
mprotect((void*)addr3, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC);
mprotect((void*)addr4, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC);

DobbyHook((void *)dlsym(handle,"open"), (void *)fake_open, (void **)&orig_open);
DobbyHook((void *)dlsym(handle,"openat"), (void *)fake_openat, (void **)&orig_openat);
DobbyHook((void *)fopen, (void *)fake_fopen, (void**)&orig_fopen);
DobbyHook((void *)syscall, (void *)fake_syscall, (void **)&orig_syscall);
}

具体实现

查找到check_crc的调用地方

点击罗盘

image-20240124151147745

长按check_crc

image-20240124151213668

即可找到目的方法

aaa

然后在开头写入如下代码,注意这个只是调用代码,实际的重定向的代码是一个so中的代码,作者已经写入到安装包了,我们只需要调用即可,后续需要自己将so打入apk

1
2
3
4
5
6
7
sget-object p10, Lcom/zj/wuaipojie/util/ContextUtils;->INSTANCE:Lcom/zj/wuaipojie/util/ContextUtils;  

invoke-virtual {p10}, Lcom/zj/wuaipojie/util/ContextUtils;->getContext()Landroid/content/Context;

move-result-object p10

invoke-static {p10}, Lcom/zj/wuaipojie/util/SecurityUtil;->hook(Landroid/content/Context;)V

随后在软件数据目录中新建files文件夹,放入原安装包

image-20240124151726068

重新命名为base.apk

image-20240124151959135

在我的测试中,hash可以过,crc过不了

反调试

反调试方法

  • 安卓自带的

    1
    2
    3
    4
    5
    6
    fun checkForDebugger() {  
    if (Debug.isDebuggerConnected()) {
    // 如果调试器已连接,则终止应用程序
    System.exit(0)
    }
    }
  • debuggable属性

    先检测手机的debuggable属性,没有检测到再检测APK内部的xml的那个debuggable属性

  • ptrace检测

    1
    2
    3
    4
    int 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
    34
    int 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

    image-20240126090748593

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

    image-20240126090911296

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

    image-20240126092212041

    root检测部分也是一样的,返回值赋值为0即可

  • API校验:

    即获取包的签名信息,转换为字符串之后进行base64编码,然后md5输出

    image-20240126093820523

    如下所示

    image-20240126093838967

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

  • CRC校验:

    详见代码

    image-20240126094504066

    即校验classes.dex文件,这里通用可以返回值赋值为1解决。

    但是这里有个try-catch,并且catch始终返回为true。那么可以尝试让try中出错,进而跳转到catch,这里让classes.dex字符串修改为classes2.dex,这样找不到这个文件自然会出错,跳转到catch。

  • PM校验:详见PM代理部分

  • Hash校验:

    image-20240126095100866

    直接用返回1解决

  • SO层校验

    image-20240126101106229

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

    image-20240126101125281

    整体通过

课程七/八、Sorry,会Hook真的可以为所欲为-Xposed快速上手

环境配置

创建空白的android项目,使用java和android7作为目标

image-20240112091059961

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

image-20240112091212033

image-20240112091325824

修改xml文件配置,src->main->AndroidManifest.xml,修改如下

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
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.XposedPro"
tools:targetApi="31">
<!-- 是否是xposed模块,xposed根据这个来判断是否是模块 -->
<meta-data
android:name="xposedmodule"
android:value="true" />
<!-- 模块描述,显示在xposed模块列表那里第二行 -->
<meta-data
android:name="xposeddescription"
android:value="这是一个Xposed模块" />
<!-- 最低xposed版本号(lib文件名可知) -->
<meta-data
android:name="xposedminversion"
android:value="89" />
</application>

</manifest>

修改build.gradle.kts

1
2
3
implementation(files("libs\\XposedBridgeAPI-89.jar"))
修改为
compileOnly(files("libs\\XposedBridgeAPI-89.jar"))

区别在于implementation 使用该方式依赖的库将会参与编译和打包 compileOnly 只在编译时有效,不会参与打包

然后app新建目录,new->Folder->Assets Folder

image-20240112092036425

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

image-20240112092203496

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

image-20240112092318996

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

image-20240112092351263

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

image-20240112092504885

HOOK代码

hook指定包

首先指定需要hook的包

1
2
3
4
5
6
7
8
public class Hook implements IXposedHookLoadPackage {
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {
if(!loadPackageParam.packageName.equals("com.zj.wuaipojie")){
return;
}
}
}

添加了if语句代表只hook掉com.zj.wuaipojie这个apk

hook指定函数

在jadx中可以选定某个方法,复制为xposed片段,比如这里选择hook掉a方法

image-20240112093644001

复制之后放入if语句后面即可,需要导入包,然后在修改classLoader,添加前缀loadPackageParam.classLoader

1
2
3
4
5
6
7
8
9
10
XposedHelpers.findAndHookMethod("com.zj.wuaipojie.Demo", loadPackageParam.classLoader, "a", java.lang.String.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
}
});

即传入类,classLoader,方法名称,参数类型,然后重写beforeHookedMethod和afterHookedMethod回调函数即可

或者直接用反射来获取,通常可以用来hook自定义或者复杂函数

1
2
3
4
5
6
7
8
Class a = loadPackageParam.classLoader.loadClass("类名");
XposedBridge.hookAllMethods(a, "方法名", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);

}
});

参数处理

在beforeHookedMethod中处理参数

1
2
3
String a = "aaaaaaaa";
XposedBridge.log(param.args[0].toString()); //打印参数
param.args[0] = a; //修改参数

在afterHookedMethod中处理返回值

1
2
Log.d("zj2595",param.getResult().toString()); //获取返回值
param.setResult("123456"); //设置返回值

替换函数

类似的

1
2
3
4
5
6
7
Class a = classLoader.loadClass("类名")
XposedBridge.hookAllMethods(a,"方法名",new XC_MethodReplacement() {
home.php?mod=space&uid=1892347
protected Object replaceHookedMethod(MethodHookParam methodHookParam) throws Throwable {
return "";
}
});

通杀大部分免费加壳

针对一些加固的APP,需要先获取到classloader才能接着hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
XposedHelpers.findAndHookMethod(Application.class, "attach", Context.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
Context context = (Context) param.args[0];
ClassLoader classLoader = context.getClassLoader();

//hook逻辑在这里面写
XposedHelpers.findAndHookMethod("com.zj.wuaipojie.Demo", loadPackageParam.classLoader, "a", java.lang.String.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
}
});
}
});

处理变量

没有的就用Object

string->Object

  • 静态变量

    static的,类被初始化,同步进行初始化

    1
    2
    final Class clazz = XposedHelpers.findClass("类名", loadPackageParam.classLoader); 
    XposedHelpers.setStaticIntField(clazz, "变量名", 999);
  • 实例变量

    类被实例化(产生一个对象的时候),进行初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    final Class clazz = XposedHelpers.findClass("类名", classLoader);  
    XposedBridge.hookAllConstructors(clazz, new XC_MethodHook() { //先hook构造函数
    @Override
    protected void afterHookedMethod(MethodHookParam param) throws Throwable { //在构造函数初始化变量之后
    super.afterHookedMethod(param);
    //param.thisObject获取当前所属的对象
    Object ob = param.thisObject;
    XposedHelpers.setIntField(ob,"变量名",9999);
    }
    });

处理构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//无参构造函数,没有传入参数
XposedHelpers.findAndHookConstructor("com.zj.wuaipojie.Demo", loadPackageParam.classLoader, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
}
});

//有参构造函数,传入了参数string.class
XposedHelpers.findAndHookConstructor("com.zj.wuaipojie.Demo", loadPackageParam.classLoader, String.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
}
});

处理Dex文件

一个dex中最多只能存在65535个方法,如果超过这个数,就会重新生成dex文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
XposedHelpers.findAndHookMethod(Application.class, "attach", Context.class, new XC_MethodHook() {  
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
ClassLoader cl= ((Context)param.args[0]).getClassLoader();
Class<?> hookclass=null;
try {
hookclass=cl.loadClass("类名");
}catch (Exception e){
Log.e("zj2595","未找到类",e);
return;
}
XposedHelpers.findAndHookMethod(hookclass, "方法名", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
}
});
}
});

函数主动调用

  • 静态函数

    1
    2
    Class clazz = XposedHelpers.findClass("类名",loadPackageParam.classLoader);
    XposedHelpers.callStaticMethod(clazz,"方法名",参数(非必须));
  • 实例函数

    1
    2
    Class clazz = XposedHelpers.findClass("类名",loadPackageParam.classLoader);
    XposedHelpers.callMethod(clazz.newInstance(),"方法名",参数(非必须));

内部类

类中还有一个class,其实就是加入$符号来寻找类

1
2
3
4
5
6
7
8
XposedHelpers.findAndHookMethod("com.zj.wuaipojie.Demo$InnerClass", loadPackageParam.classLoader, "innerFunc",String.class,  new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
param.args[0] = "111111";

}
});

反射调用

即先hook住某个函数,然后找到类之后对应调用即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Class clazz = XposedHelpers.findClass("com.zj.wuaipojie.Demo", loadPackageParam.classLoader);
XposedHelpers.findAndHookMethod("com.zj.wuaipojie.Demo$InnerClass", loadPackageParam.classLoader, "innerFunc",String.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
//第一步找到类
//找到方法,如果是私有方法就要setAccessible设置访问权限
//invoke主动调用或者set修改值(变量)
Class democlass = Class.forName("com.zj.wuaipojie.Demo",false,loadPackageParam.classLoader);
Method demomethod = democlass.getDeclaredMethod("refl");
demomethod.setAccessible(true);
demomethod.invoke(clazz.newInstance());
}
});

遍历包中所有类下所有方法

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
XposedHelpers.findAndHookMethod(ClassLoader.class, "loadClass", String.class, new XC_MethodHook() {  
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
Class clazz = (Class) param.getResult();
String clazzName = clazz.getName();
//排除非包名的类
if(clazzName.contains("com.zj.wuaipojie")){
Method[] mds = clazz.getDeclaredMethods();
for(int i =0;i<mds.length;i++){
final Method md = mds[i];
int mod = mds[i].getModifiers();
//去除抽象、native、接口方法
if(!Modifier.isAbstract(mod)
&& !Modifier.isNative(mod)
&&!Modifier.isAbstract(mod)){
XposedBridge.hookMethod(mds[i], new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
Log.d("zj2595",md.toString());
}
});
}

}
}

}
});

堆栈查询

定位”已过期”字符串赋值部分的堆栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
XposedHelpers.findAndHookMethod("android.widget.TextView", loadPackageParam.classLoader, "setText", CharSequence.class, new XC_MethodHook() {  
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
Log.d("zj2595",param.args[0].toString());
if(param.args[0].equals("已过期")){
printStackTrace();
}
}
});
private static void printStackTrace() {
Throwable ex = new Throwable();
StackTraceElement[] stackElements = ex.getStackTrace();
for (int i = 0; i < stackElements.length; i++) {
StackTraceElement element = stackElements[i];
Log.d("zj2595","at " + element.getClassName() + "." + element.getMethodName() + "(" + element.getFileName() + ":" + element.getLineNumber() + ")");
}
}

事件监听

检测点击事件对应的方法

1
2
3
4
5
6
7
8
9
10
11
Class clazz = XposedHelpers.findClass("android.view.View", loadPackageParam.classLoader);
XposedBridge.hookAllMethods(clazz, "performClick", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
Object listenerInfoObject = XposedHelpers.getObjectField(param.thisObject, "mListenerInfo");
Object mOnClickListenerObject = XposedHelpers.getObjectField(listenerInfoObject, "mOnClickListener");
String callbackType = mOnClickListenerObject.getClass().getName();
Log.d("zj2595",callbackType);
}
});

布局改写

1
2
3
4
5
6
7
8
9
10
11
XposedHelpers.findAndHookMethod("com.zj.wuaipojie.ui.ChallengeSixth", loadPackageParam.classLoader,  
"onCreate", Bundle.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
View img = (View)XposedHelpers.callMethod(param.thisObject,
"findViewById", 0x7f0800de);
img.setVisibility(View.GONE);

}
});

LSPosed免root

使用https://github.com/LSPosed/LSPatch即可

先安装模块,然后打开LPatch,在应用栏添加应用

image-20240116105811518

选择已安装的APK,或者存储目录都行

  • 本地模式:只能在本地设备运行,相当于还是hook

  • 集成模式:将模块嵌入到APP中,可以在其他手机运行,这个其实也会修改签名

    image-20240116110011670

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

    image-20240116110036790

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

    image-20240116110105910

    修补后会在之前选定的存储目录中生成对应的APP,此时安装即可。

简单HOOK

有一些简单hook的教程,还有Xposed的源码解析

GitHub - littleWhiteDuck/SimpleHook: SimpleHook hook部分代码

十、不是我说,有了IDA还要什么女朋友?

ELF就不介绍了

NDK开发

在Frida逆向与抓包实战有介绍

课后作业

如之前所分析的,调用的是so里面的getSercet函数,打开IDA分析,修改函数如下返回值

image-20240126102319382

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

image-20240126102505349

十二、大佬帮我分析一下

so文件加载流程

img

函数名 描述
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
2
3
4
adb shell am start -D -n com.zj.wuaipojie/.ui.ChallengeEight (去掉-D 则表示不以debug模式启动app)
adb forward tcp:23946 tcp:23946 (端口转发)
adb forward tcp:8700 jdwp:PID (pid监听)
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700 (jdb挂起)

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
2
3
4
5
6
function main(){
Java.perform(function(){
hookTest1();
});
}
setImmediate(main);

hook类普通方法

如下即重新实现com.zj.wuaipojie.Demo.a函数,打印参数和返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
//定义一个名为hookTest1的函数
function hookTest1(){
//获取一个名为"类名"的Java类,并将其实例赋值给JavaScript变量utils
var utils = Java.use("com.zj.wuaipojie.Demo");
//修改"类名"的"method"方法的实现。这个新的实现会接收两个参数(a和b)
utils.a.implementation = function(str){
var retval = this.a(str);
//在控制台上打印参数a,b的值以及"method"方法的返回值
console.log(str,retval);
//返回"method"方法的返回值
return retval;
}
}

在这里面修改参数,return返回值就可以修改这个函数了。

hook重载函数

如果方法是重载的,那么需要设置一下具体的参数,比如上面的函数a是重载的,那么需要设定如下

1
2
3
4
5
6
7
8
9
10
11
function hookTest1(){
//获取一个名为"类名"的Java类,并将其实例赋值给JavaScript变量utils
var utils = Java.use("com.zj.wuaipojie.Demo");
//修改"类名"的"method"方法的实现。这个新的实现会接收两个参数(a和b)
utils.a.overload("java.lang.String").implementation = function(str){
var retval = this.a(str);
console.log(str,retval);
//返回"method"方法的返回值
return retval;
}
}

即添加overload指定对应重载函数的参数来确定该函数

hook重载自定义参数类型的函数

借助smali代码来查看,比如如下Inner函数中的Animal参数类型

image-20240118111631902

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

image-20240118111824561

对应hook代码如下,修改第二个参数为”aaaaaaaaaa”

1
2
3
4
5
6
7
8
9
function hookTest1(){
var utils = Java.use("com.zj.wuaipojie.Demo");
//overload定义重载函数,根据函数的参数类型填
utils.Inner.overload('com.zj.wuaipojie.Demo$Animal','java.lang.String').implementation = function(a,b){
b = "aaaaaaaaaa";
this.Inner(a,b);
console.log(b);
}
}

hook构造函数

用$init来表示构造函数,同样重载的构造函数也是类似的

1
2
3
4
5
6
7
8
9
function hookTest3(){
var utils = Java.use("com.zj.wuaipojie.Demo");
//修改类的构造函数的实现,$init表示构造函数
utils.$init.overload('java.lang.String').implementation = function(str){
console.log(str);
str = "52";
this.$init(str);
}
}

hook变量

  • 静态变量
1
2
utils.staticField.value = "我是被修改的静态变量";
console.log(utils.staticField.value);
  • 实际变量

注意字段名与函数名相同 前面加个下划线

1
2
3
4
5
6
7
8
9
Java.choose("com.zj.wuaipojie.Demo", {
onMatch: function(obj){
//修改实例的非静态字段"_privateInt"的值为"123456",并修改非静态字段"privateInt"的值为9999。
//obj._privateInt.value = "123456"; //字段名与函数名相同 前面加个下划线
obj.privateInt.value = 9999;
},
onComplete: function(){
}
});

实际上在这个例子里面,大佬说hoos是发生在obj.test()函数调用之后的,但是也很奇怪啊,如果是真的,那么应该是obj.test()函数之前的所有数据均无法被更改,反正很奇怪。

hook内部类

一样的,就是获取到目标类就行

1
2
3
4
5
6
7
8
9
10
11
function hookTest6(){
Java.perform(function(){
//内部类
var innerClass = Java.use("com.zj.wuaipojie.Demo$innerClass");
console.log(innerClass);
innerClass.$init.implementation = function(){
console.log("eeeeeeee");
}

});
}

枚举所有类与所有方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function hookTest7(){
Java.perform(function(){
//枚举所有的类与类的所有方法,异步枚举
Java.enumerateLoadedClasses({
onMatch: function(name,handle){
//过滤类名
if(name.indexOf("com.zj.wuaipojie.Demo") !=-1){
console.log(name);
var clazz =Java.use(name);
console.log(clazz);
var methods = clazz.class.getDeclaredMethods();
console.log(methods);
}
},
onComplete: function(){}
})
})
}

枚举所有方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function hookTest8(){
Java.perform(function(){
var Demo = Java.use("com.zj.wuaipojie.Demo");
//getDeclaredMethods枚举所有方法
var methods =Demo.class.getDeclaredMethods();
for(var j=0; j < methods.length; j++){
var methodName = methods[j].getName();
console.log(methodName);
for(var k=0; k<Demo[methodName].overloads.length;k++){
Demo[methodName].overloads[k].implementation = function(){
for(var i=0;i<arguments.length;i++){
console.log(arguments[i]);
}
return this[methodName].apply(this,arguments);
}
}
}
})
}

主动调用

  • 静态方法

    1
    2
    var ClassName=Java.use("com.zj.wuaipojie.Demo"); 
    ClassName.privateFunc("传参");
  • 非静态方法

    和变量类似,都是要获取对应类的对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var 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:即开始探索,会直接启动应用

    image-20240118150720659

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

    image-20240118151237102

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

    image-20240118150732300

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

    image-20240118150751024

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

    image-20240118150813676

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

    image-20240118151512199

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

    image-20240118151701274

  • 关闭ssl校验:android sslpinning disable

    image-20240118152005591

  • 关闭root校验:android root disable

    image-20240118152022712

Objection内存漫游

从内存中查看数据

  • android heap search instances 类名(命令):查看某个类对象

    这是内存中没加载的情况

    image-20240118152210186

    有加载的是如下情况

    image-20240118152358178

  • android heap execute [hashcode] getPublicInt(实例的hashcode+方法名) :调用实例的方法

    • 无参函数

      image-20240118152604269

    • 有参函数

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

      image-20240118152850895

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

    image-20240118153051217

  • android hooking search classes 关键类名 -在内存中所有已加载的类中搜索包含特定关键词的类,以wuaipojie为关键词

    image-20240118153217665

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

  • android hooking list class_methods 类名 -内存漫游类中的所有方法

    image-20240118153340743

Objection的HOOK

这个HOOK并不会使得这些方法失效,相当于只是查看

  • android hooking watch class 类名 :hook类的所有方法

    image-20240118153605750

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

    image-20240118154010669

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

    image-20240118154626876

十五、是时候学习一下Frida一把梭了(下)

打印导入表/导出表

貌似需要frida14,frida16可能写法有点出入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function hookTest1(){
Java.perform(function(){
//打印导入表
var imports = Module.enumerateImports("lib52pojie.so");
for(var i =0; i < imports.length;i++){
if(imports[i].name == "vip"){
console.log(JSON.stringify(imports[i])); //通过JSON.stringify打印object数据
console.log(imports[i].address);
}
}
//打印导出表
var exports = Module.enumerateExports("lib52pojie.so");
for(var i =0; i < exports.length;i++){
console.log(JSON.stringify(exports[i]));
}

})
}

静态注册JNI函数HOOK

  • JAVA层,同样也是获取到类之后重新实现一下函数就行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function 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
    23
    function 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

    image-20240118172755247

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
2
3
var moduleAddr1 = Process.findModuleByName("lib52pojie.so").base;  
var moduleAddr2 = Process.getModuleByName("lib52pojie.so").base;
var moduleAddr3 = Module.findBaseAddress("lib52pojie.so");

SO未导出函数与函数地址计算

可能存在函数运行,但是没有设置JNI静态导出类型的,只是在SO层内部使用,这时候可以通过加载地址来调用,同样对于JNI动态注册类型也适用。

  1. 安卓里一般32 位的 so 中都是thumb指令,64 位的 so 中都是arm指令

  2. 通过IDA里的opcode bytes来判断

    arm 指令为 4 个字节(options -> general -> Number of opcode bytes (non-graph) 输入4)

    thumb指令不确定的,有的4字节,有的2字节

  3. 地址计算:

    thumb 指令,函数地址计算方式: so 基址 + 函数在 so 中的偏移 +1

    arm 指令,函数地址计算方式: so 基址 + 函数在 so 中的偏移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function hookTest6(){
Java.perform(function(){
//根据导出函数名打印基址
var soAddr = Module.findBaseAddress("lib52pojie.so");
console.log(soAddr);
var funcaddr = soAddr.add(0x1071C);
console.log(funcaddr);
if(funcaddr != null){
Interceptor.attach(funcaddr,{
onEnter: function(args){ //args参数

},
onLeave: function(retval){ //retval返回值
console.log(retval.toInt32());
}
})
}
})
}

Hook掉dlopen

dlopen是用来加载so文件的

image-20240119092246579

即可以通过hook时判断参数filename是否为我们想要的.so文件,来指定Hook到.so文件加载时运行的函数逻辑,这里即为关注hook加载lib52pojie.so文件的函数逻辑

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
function hook_dlopen() {
//这是低版本的android
var dlopen = Module.findExportByName(null, "dlopen");
Interceptor.attach(dlopen, {
onEnter: function (args) {
var so_name = args[0].readCString();
if (so_name.indexOf("lib52pojie.so") >= 0) this.call_hook = true;
}, onLeave: function (retval) {
if (this.call_hook) hookTest2();
}
});

// 高版本Android系统使用android_dlopen_ext
var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");
Interceptor.attach(android_dlopen_ext, {
onEnter: function (args) {
var so_name = args[0].readCString();
if (so_name.indexOf("lib52pojie.so") >= 0) this.call_hook = true;
}, onLeave: function (retval) {
if (this.call_hook)
//这里实际hook在加载lib52pojie.so时候的逻辑为hookTest2();
hookTest2();
}
});
}

十六、是时候学习一下Frida一把梭了(终)

Frida写数据

一般用在脱壳当中

1
2
3
4
5
6
7
8
//一般写在app的私有目录里,不然会报错:failed to open file (Permission denied)(实际上就是权限不足)
var file_path = "/data/user/0/com.zj.wuaipojie/test.txt";
var file_handle = new File(file_path, "wb");
if (file_handle && file_handle != null) {
file_handle.write(data); //写入数据
file_handle.flush(); //刷新
file_handle.close(); //关闭
}

inlinehook内联

写法和函数地址计算时的写法一样的,其实就是定位到某条汇编,然后进行Hook。

我感觉函数层面的原理就是基于这个的,只是当检测为函数时,就可以操作args了。而且这个onLeave,可能检测的是汇编 RET指令,然后修改返回的X0寄存器作为返回值,因为在这个例子里面,onLeave也能修改适用。

这里Hook的是check函数中的如下位置

image-20240119111302320

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function inline_hook(){
Java.perform(function(){
//根据导出函数名打印基址
var soAddr = Module.findBaseAddress("lib52pojie.so");
console.log(soAddr);
var funcaddr = soAddr.add(0x010428);
console.log(funcaddr);
if(funcaddr != null){
console.log("get");
Interceptor.attach(funcaddr,{
onEnter: function(args){ //args参数
console.log(this.context.x22); //注意此时就没有args概念了
this.context.x22 = ptr(1); //赋值方法参考上一节课
},
onLeave: function(retval){ //retval返回值
console.log(retval.toInt32());
retval.replace(1);
}
})
}
})
}

这里Hook到1042C,然后给X0赋值为1也是一样的。那么其实对于Frida针对So层的Hook,其实具有更加细腻的汇编层面,那这个可以修改汇编代码吗?

汇编层面

[Online ARM to HEX Converter (armconverter.com)](https://armconverter.com/?code=mov w0,1)

  • 读取

    1
    2
    3
    var soAddr = Module.findBaseAddress("lib52pojie.so");
    var codeAddr = Instruction.parse(soAddr.add(0x10428));
    console.log(codeAddr.toString());

image-20240119111659002

  • 写入

    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
    function 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
    13
    Error: 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
2
3
4
5
6
7
var funcAddr = Module.findBaseAddress("lib52pojie.so").add(0xe85c);
//声明函数指针
//NativeFunction的第一个参数是地址,第二个参数是返回值类型,第三个[]里的是传入的参数类型(有几个就填几个)
var aesAddr = new NativeFunction(funcAddr , 'pointer', ['pointer', 'pointer']);
var encry_text = Memory.allocUtf8String("OOmGYpk6s0qPSXEPp4X31g=="); //开辟一个指针存放字符串
var key = Memory.allocUtf8String('wuaipojie0123456');
console.log(aesAddr(encry_text ,key).readCString());

对应的数据类型如下

参考JavaScript API | Frida • A world-class dynamic instrumentation toolkit

数据类型 描述
void 无返回值
pointer 指针
int 整数
long 长整数
char 字符
float 浮点数
double 双精度浮点数
bool 布尔值

番外

去除签名校验

手动去除,搜索app kill,找到第一个,进入查找f函数,修改f函数后保存

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
.method public static f()Ljava/util/List;
.registers 2
.annotation build Landroidx/annotation/NonNull;
.end annotation

.annotation system Ldalvik/annotation/Signature;
value = {
"()",
"Ljava/util/List<",
"Ljava/lang/String;",
">;"
}
.end annotation

.line 1
new-instance v0, Ljava/util/ArrayList;

invoke-direct {v0}, Ljava/util/ArrayList;-><init>()V

const-string v1, "F9:6C:E9:5F:D5:47:BE:DF:81:15:E3:71:8A:10:54:45"
#创建了一个新的 ArrayList 实例,然后向列表中添加了一个字符串常量 "F9:6C:E9:5F:D5:47:BE:DF:81:15:E3:71:8A:10:54:45"

invoke-virtual {v0, v1}, Ljava/util/ArrayList;->add(Ljava/lang/Object;)Z

return-object v0
.end method

去除更新弹窗

安卓逆向入门笔记

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
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
71
72
73
74
75
76
77
78
79
80
###### Class com.p005zj.wuaipojie.p006ui.MyActivity (com.zj.wuaipojie.ui.MyActivity)
.class public Lcom/zj/wuaipojie/ui/MyActivity;
.super Landroidx/appcompat/app/AppCompatActivity;
.source "MyActivity.java"

# direct methods
.method public constructor <init>()V
.registers 1

.line 7
invoke-direct {p0}, Landroidx/appcompat/app/AppCompatActivity;-><init>()V

return-void
.end method

# virtual methods
.method public onBackPressed()V
.registers 2

invoke-super {p0}, Landroidx/appcompat/app/AppCompatActivity;->onBackPressed()V

invoke-virtual {p0}, Landroid/app/Activity;->finish()V

return-void
.end method

.method protected onCreate(Landroid/os/Bundle;)V
.registers 6

const p1, 0x7f0b007e

invoke-virtual {p0, p1}, Lcom/zj/wuaipojie/ui/MyActivity;->setContentView(I)V

new-instance v0, Landroid/os/Handler;

invoke-direct {v0}, Landroid/os/Handler;-><init>()V

iput-object v0, p0, Lcom/zj/wuaipojie/ui/MyActivity;->handler:Landroid/os/Handler;

new-instance v0, Lcom/zj/wuaipojie/ui/MyActivity$1;

invoke-direct {v0, p0}, Lcom/zj/wuaipojie/ui/MyActivity$1;-><init>(Lcom/zj/wuaipojie/ui/MyActivity;)V

const-wide/16 v1, 0x12c

invoke-virtual {p0, v0, v1, v2}, Landroid/os/Handler;->postDelayed(Ljava/lang/Runnable;J)Z

return-void

.line 20
new-instance v0, Landroid/content/Intent;

const-class v1, Lcom/zj/wuaipojie/ui/MainActivity;

invoke-direct {v0, p0, v1}, Landroid/content/Intent;-><init>(Landroid/content/Context;Ljava/lang/Class;)V

invoke-virtual {p0, v0}, Landroid/content/Context;->startActivity(Landroid/content/Intent;)V

return-void
.end method

.method public onCreateOptionsMenu(Landroid/view/Menu;)Z
.registers 3

invoke-super {p0, p1}, Landroidx/appcompat/app/AppCompatActivity;->onCreateOptionsMenu(Landroid/view/Menu;)Z

const/4 v0, 0x1

return v0
.end method

.method public onOptionsItemSelected(Landroid/view/MenuItem;)Z
.registers 3

invoke-super {p0, p1}, Landroidx/appcompat/app/AppCompatActivity;->onOptionsItemSelected(Landroid/view/MenuItem;)Z

const/4 v0, 0x1

return v0
.end method

对应java代码为

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
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;

import androidx.appcompat.app.AppCompatActivity;

public class MyActivity extends AppCompatActivity {

private Handler handler;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my);

handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
Intent intent = new Intent(MyActivity.this, MainActivity.class);
startActivity(intent);
finish();
}
}, 5000); // 5秒后跳转
}

}

找到apk中的存放activity的位置,放入myActivity,这个包里的路径为:教程demo(更新)\smali\com\zj\wuaipojie\ui

然后添加布局文件hello_word.xml,放到教程demo(更新)\res\layout\

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="utf-8"?>
<!-- 声明 XML 文件版本和编码方式。-->
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- RelativeLayout 布局,定义根布局。-->

<TextView
android:id="@+id/text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Hello World!"
android:textSize="24sp"/>
<!-- TextView 组件,显示文本。-->

</RelativeLayout>
<!-- RelativeLayout 布局结束。-->

添加布局文件,那么还需要添加布局文件的的资源ID,在教程demo(更新)\smali\com\zj\wuaipojie\R$layout.smali文件中添加如下

image-20240105095827763

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

image-20240105100010298

c)声明Activity并默认启动

修改AndroidManifest.xml,将myActivity设置为主Activity

image-20240105102042503

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

image-20240105102136469

换成最新版本的可以成功,或者用AndroidKiller,替换一下apktool就行,但是不要改脚本,改apktool的文件名就可以。

d)签名

正常签名

完成之后,安装

Frida逆向与抓包实战

参考的书《Frida逆向与抓包实战》

环境安装

还是用bluestacks5,版本为android9bluestacks.conf不要设置只读,不然adb的端口也被固定,这样windows启动之后说不定就占了这个端口,这样就不太行了。

然后windows安装对应的python、frida、frida-tools即可,这里用的是python3.8.0

1
2
pip install frida==12.8.0
pip install frida-tools==5.3.0

然后在githubfrida下载对应版本架构的server,可以通过adb进入模拟器输入

1
getprop ro.product.cpu.abi

查看架构,这里是x86_64的,那么下载如下的frida-server

image-20230925110525614

之后将frida-server放入模拟器的/data/local/tmp中赋予权限运行即可。

然后在windowsfrida运行即可hook,有点类似gdb

1
frida -U -l .\Chap03\2.js com.example.fridapro

image-20230925110704618

Frida基础知识

HelloWorld

最基础的注入,使用如下js脚本

1
2
3
4
5
6
7
setTimeout(
function(){
Java.perform(function(){
console.log("hello world!")
})
}
)

对应注入命令

1
frida -U -l hello.js android.process.media

其中-UUSB设备,-l用于指定注入脚本路径,将代码注入到android.process.media这个进程中,即可打印出hello world!

HOOK基础

对应java代码编写的app如下

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
package com.example.fridapro;

import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;

import android.util.Log;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
while (true){

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

fun(50,30);
}
}
void fun(int x , int y ){
Log.d("r0ysue.sum" , String.valueOf(x+y));
}
}

即使用fun函数循环打印r0ysue.sum以及算数和,那么就可以使用frida对这个fun函数进行HOOK,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function main(){
console.log("Script loaded successfully ")
Java.perform(function(){
console.log("Inside java perform function")

//通过java.use获取包主类
var MainAcitivity = Java.use('com.example.fridapro.MainActivity')
console.log("Java.Use.Successfully!") //定位类成功!
// hook 重载函数,这里不用overload也可以
//但是最好还是指定一下
MainAcitivity.fun.overload('int', 'int').implementation = function(x,y){
console.log("orignal args: x => ",x,", y => ",y)
var ret_value = this.fun(2, 5);
return ret_value
}
})
}
setImmediate(main)

然后在模拟器中运行appwindows中对应的命令也是类似的,指定一下进程名即可

1
frida -U -l 1.js com.example.fridapro

使用adb logcat查看即可发现函数参数被修改

image-20230925113535171

主动调用函数

可以使用FridaAPP中没有被调用的函数进行主动调用

例子APP如下

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
package com.example.fridapro;

import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;

import android.util.Log;
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
while (true){

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

fun(50,30);
}
}
void fun(int x , int y ){
Log.d("r0ysue.sum" , String.valueOf(x+y));
}
String fun(String x){
return x.toLowerCase();
}

void secret(){
Log.d("r0ysue.secret" , "this is secret func");
}
static void staticSecret(){
Log.d("r0ysue.secret" , "this is static secret func");
}
}

加入了secrect相关函数,其中一个是全局静态的。

对应主动调用的Frida相关的代码如下

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
function main(){
console.log("Script loaded successfully ")
Java.perform(function(){
console.log("Inside java perform function")
var MainAcitivity = Java.use('com.roysue.demo02.MainActivity')

// 静态函数主动调用,比较简单
MainAcitivity.staticSecret();

// 会报错Error: secret: cannot call instance method without an instance
//MainAcitivity.secret();

// 动态函数主动调用
Java.choose('com.example.fridapro.MainActivity',{
onMatch: function(instance){
console.log('instance found',instance)
instance.secret()
},
onComplete: function(){
console.log('search Complete')
}
})
})
}
setImmediate(main)

运行之后,查看adb logcat即可看到secrect函数被调用

image-20230925155340657

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

image-20230925155517808

RPC自动化

可以使用python脚本完成js脚本对进程的注入以及hook

例子APP如下

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
package com.example.fridapro;

import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;

import android.util.Log;
public class MainActivity extends AppCompatActivity {
private String total = "hello";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
while (true){

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

fun(50,30);
}
}
void fun(int x , int y ){
Log.d("r0ysue.sum" , String.valueOf(x+y));
}
String fun(String x){
return x.toLowerCase();
}

void secret(){
total += " secretFunc";
Log.d("r0ysue.secret" , "this is secret func");
}
static void staticSecret(){
Log.d("r0ysue.secret" , "this is static secret func");
}
}

secrect函数中加入了对total属性进行处理的操作。在使用时,如果是static的变量,则直接类加变量名即可获取。如果是类里面的属性变量,则如下代码,也是需要获取类实例才可以的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function getTotalValue(){
Java.perform(function(){
var MainAcitivity = Java.use('com.roysue.demo02.MainActivity')

// 动态函数主动调用
Java.choose('com.roysue.demo02.MainActivity',{
onMatch: function(instance){
console.log('total value = ',instance.total.value)
},
onComplete: function(){
console.log('search Complete')
}
})
})
}
setImmediate(getTotalValue)

那么即可获取到变量值了

image-20230925160640781

下面尝试RPC自动化,在上述js代码中加入CallSecrectFunc来调用secrect函数,如下代码。再加上rpc导出代码,方便后续python复用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function CallSecretFunc(){
Java.perform(function(){

// 动态函数主动调用
Java.choose('com.roysue.demo02.MainActivity',{
onMatch: function(instance){

instance.secret()

},
onComplete: function(){
}
})
})
}

rpc.exports = {
callsecretfunc : CallSecretFunc,
gettotalvalue : getTotalValue
}

然后再运行导出即可

TIPS

  • 查看运行的包:frida-ps -U

    image-20230925111121983

  • 当函数被重载之后,使用Frida进行注入则需要指定参数类型,如下

    1
    2
    3
    MainAcitivity.fun.overload('int', 'int').implementation = function(x,y){
    //....
    }

Objection逆向

Cha05破解弹窗

Android中常用

Native层HOOK

NDK开发

新建Android studio项目,选择Native C++项目,响应的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
package com.example.ndkpro;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.widget.TextView;

import com.example.ndkpro.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {

// Used to load the 'ndkpro' library on application startup.
static {
System.loadLibrary("ndkpro");
}

private ActivityMainBinding binding;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());

// Example of a call to a native method
TextView tv = binding.sampleText;
tv.setText(stringFromJNI());
}

/**
* A native method that is implemented by the 'ndkpro' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
}

下面进行一些简单的解析:

  • System.loadLibrary("ndkpro"):代表将这个动态库加载到内存中

  • public native String stringFromJNI():这个函数实际上是一个JNI函数,真实的函数内容是通过C/C++进行实现的,点击C/C++图标可以跳转查看真实代码

    image-20230926112422331

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <jni.h>
    #include <string>

    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 *envjobject分别表示当前java线程的执行环境和响应函数所在类的对象。

  • 此外还有一些对应的数据类型

    Java数据类型 JNI数据类型
    boolean jboolean
    byte jbyte
    void void
    ….
    class jclass

    除了void,大多都是在前面加j

直接使用这个例子编译为APK,解压打开APK会发现多一个lib目录,下面存放四种架构的.so文件,在对应的平台调用不同架构的.so文件

image-20230926113457997

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

image-20230926113754950

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

image-20230926114035756

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

image-20230926114143365

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

image-20230926114600807

开始HOOK

静态注册JNI函数

上述提到的都是静态注册的JNI函数,有一定的命名方式

修改一下例子,使之循环调用需要Hook的函数

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
package com.example.ndkpro;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.widget.TextView;

import android.util.Log;
import com.example.ndkpro.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {

// Used to load the 'ndkpro' library on application startup.
static {
System.loadLibrary("ndkpro");
}

private ActivityMainBinding binding;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

// Example of a call to a native method
TextView tv = findViewById(R.id.sample_text);
tv.setText(sI3());

while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

Log.i("r0so2", stringFromJNI());
}
}

/**
* A native method that is implemented by the 'ndkpro' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
}

针对上述例子中的libndkpro.so,使用如下代码进行HOOK

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function hook_native(){
var addr = Module.getExportByName("libndkpro.so", "Java_com_example_ndkpro_MainActivity_stringFromJNI");
Interceptor.attach(addr,{
onEnter:function(args){
console.log("jnienv pointer =>",args[0])
console.log("jobj pointer =>",args[1])
},onLeave:function(retval){
console.log("retval is =>",Java.vm.getEnv().getStringUtfChars(retval, null).readCString())
console.log("=================")
}
})
}

setImmediate(hook_native)

正常使用frida进行hook即可。

动态注册JNI函数

修改一下模式即可,在native-lib.cpp中更改如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
jstring JNICALL sI3(
JNIEnv* env,
jobject /*this*/) {
std::string hello = "Hello from C++ stringFromJNI3 r0ysue ";
return env->NewStringUTF(hello.c_str());
}

jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
JNIEnv * env;
vm->GetEnv((void**)&env,JNI_VERSION_1_6);
JNINativeMethod methods[] = {
{"stringFromJNI3","()Ljava/lang/String;",(void*)sI3},
};

env->RegisterNatives(env->FindClass("com/example/ndkpro/MainActivity"),methods,1);

return JNI_VERSION_1_6;
}

这里即代表对stringFromJNI3的函数调用注册为对sI3的函数调用,在对应的Mainactivity中还是不变,修改成调用stringFromJNI3即可。重新编译运行APP即可看到对应的adb logcat日志

image-20230927153610314

使用这种方式注册的JNI函数的ndkpro.so会找不到stringFromJNI3()字符串相关的函数,这种情况就需要通过先寻找模块基地址,然后寻找偏移地址来获取。

mark:书里面提供的脚本用不了

移动安全升级

第一课、截屏,加壳

破解mmzztt的截屏,首先打开mmzztt,然后用adb即可查看当前apk的一些包信息adb shell dumpsys window | grep mCurrentFocus,注意这里搜索到的com.mmzztt.app是包的名称,用来使用spawn模式重新启动进程,但是实际上启动之后的进程可能并不叫这个名称,可能是其他名称,如果使用attach模式的话,则需要进程名称才行的。

image-20231007104116697

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

比较关键的就是如下一段代码

image-20231007140745804

那么就可以搜索一下这个setFlasgAPI是否存在调用,直接搜索函数

1
android hooking search methods setFlags

会出现一些结果,但是都不是window相关的函数调用

image-20231007140600567

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

1
android hooking search classes window

image-20231007140952899

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

1
android hooking list class_methods android.view.Window

image-20231007141516805

存在该方法调用,那么就可以hook这个方法,使其不发生作用即可,这里直接用objection好像不太好使,接下来转换为r0tracer进行寻找

r0ysue/r0tracer: 安卓Java层多功能追踪脚本 (github.com)

需要修改一下里面的代码,改为指定函数类即可

image-20231007142540131

然后如下代码

1
frida -U -f com.mmzztt.app -l r0tracer.js -o mzt.logs

-fspawn模式,因为可能在程序运行之后,这个函数已经被调用了,后续不会被调用,那么就需要用这个模式来重启运行,最终在日志中找到如下调用链

image-20231007143001688

依据这个来hook或者其他方式即可,使用如下代码进行hook并打印相关信息

1
2
3
4
5
6
7
8
9
10
setImmediate(function(){
Java.perform(function(){
Java.use("android.view.Window").setFlags.implementation = function(x){
console.log("setFlags called")
}
// Java.use("android.view.Window").addFlags.implementation = function(x,y){
// console.log("addFlags called")
// }
})
})

这样frida -U -f com.mmzztt.app -l ./mmzztt.js使用spawn模式启动之后,这样就能截屏了,完成破解

image-20231007145600476

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

image-20231007143201603

那么使用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即可

image-20231007151049150

然后搜索一下

image-20231007151303850

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

image-20231007151426203

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

image-20231007151552568

F5重新加载一下即可

image-20231007151659442

第二课、攻防世界基础android

hook逆向基础,拉起activity,广播

攻防世界 (xctf.org.cn)下载APK安装,发现需要输入密码,逆向分析找到Mainactivity,找到button执行逻辑,其实就是调用check.checkPassword

image-20231007160215264

查看一下代码

image-20231007161409585

那么hook一下返回true即可

1
2
3
4
5
6
7
8
9
setImmediate(function(){
Java.perform(function(){
Java.use("com.example.test.ctf02.Check").checkPassword.implementation = function(x){
console.log("check called");
var ret_value = true;
return ret_value;
}
})
})

使用如下命令frida -U -f com.example.test.ctf02 -l ./test.js

然后进入下一关,需要输入然后显示

image-20231007161609398

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

image-20231007162115632

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

image-20231007162357541

再查看广播对应代码,

image-20231007162458278

调用NextContent

image-20231007162542913

onCreate调用到Change

image-20231007162524862

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

image-20231007162709557

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

image-20231007162815506

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

image-20231007163211614

这里的MainActivity2也能使用这种方式拉起,相当于可以直接绕过关卡。

第三课、六层锁机

第一关 获取函数返回值

打开APK,查看AndroidManifest.xml查找主Activity

image-20231007173222240

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

image-20231008090708333

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

1
2
objection -g com.example.androiddemo explore
android hooking watch class_method com.example.androiddemo.Activity.LoginActivity.a --dump-args --dump-backtrace --dump-return

即可获取到

image-20231008093318794

第二关 hook函数返回值

来到BaseFridaActivity,点击按钮调用到onClick,调用到FridaActivity1onCheck,查看一下代码

image-20231008093628716

放入a函数加密之后需要为R4jSLLLLLLLLLLOrLE7/5B+Z6fsl65yj6BgC6YWz66gO6g2t65Pk6a+P65NK44NNROl0wNOLLLL=,那么hook掉这个函数的返回值即可

1
2
3
4
5
6
7
8
9
10
11
12
function one(){
Java.perform(function(){
let FridaActivity1 = Java.use("com.example.androiddemo.Activity.FridaActivity1");
FridaActivity1["a"].implementation = function(str){
console.log("a called");
return "R4jSLLLLLLLLLLOrLE7/5B+Z6fsl65yj6BgC6YWz66gO6g2t65Pk6a+P65NK44NNROl0wNOLLLL=";
}
})
}


setImmediate(one)

第三关 设置值,函数调用

来到下一关FridaActivity2

image-20231008095316288

那么hook一下onCheck函数,将两个属性置为True即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function two(){
Java.perform(function(){
let FridaActivity2 = Java.use("com.example.androiddemo.Activity.FridaActivity2");

FridaActivity2["onCheck"].overload().implementation = function(){
console.log("onCheck called");
this.static_bool_var.value = true;
this.bool_var.value = true;
console.log("static_bool_var:",this.static_bool_var);
console.log("bool_var:",this.bool_var);
this.onCheck();

}
})
}

也可以不用hookonCheck函数,可以直接设置值的

1
2
3
4
5
6
7
8
9
10
11
12
13
function two(){
Java.perform(function(){
let FridaActivity2 = Java.use("com.example.androiddemo.Activity.FridaActivity2");
FridaActivity2.static_bool_var.value = true;
Java.choose("com.example.androiddemo.Activity.FridaActivity2",{
onMatch:function(ins){
ins.bool_var.value = true
},onComplete:function(){
}
});

})
}

也可以调用接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function two(){
Java.perform(function(){
let FridaActivity2 = Java.use("com.example.androiddemo.Activity.FridaActivity2");
FridaActivity2.setStatic_bool_var();
Java.choose("com.example.androiddemo.Activity.FridaActivity2",{
onMatch:function(ins){
ins.setBool_var();
},onComplete:function(){

}
})

})
}

关键知识点就是当属性或者函数需要借助于对象时,那么就需要先获取一个对象才能调用或者设置值。静态的使用java.use获取到类就可以直接使用。

第四关 函数名变量名相同

进入到FridaActivity4

image-20231008102043873

同样直接设置一下值即可,需要注意的时直接当变量属性和函数名称一样时,找对应的变量名称在前面需要加上_下划线,如下代码所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function three(){
Java.perform(function(){
let FridaActivity3 = Java.use("com.example.androiddemo.Activity.FridaActivity3");
FridaActivity3["onCheck"].overload().implementation = function(){
console.log("onCheck called");
this.static_bool_var.value = true;
this.bool_var.value = true;
this._same_name_bool_var.value = true;
console.log("static_bool_var:",this.static_bool_var.value);
console.log("bool_var:",this.bool_var.value);
console.log("same_name_bool_var:",this._same_name_bool_var.value);
this.onCheck();
}
})
}

setImmediate(three)

第五关 java反射,静态内部类

进入FridaAacvitity4

image-20231008103403390

hook掉所有check函数返回true即可,这个我写出来不行,还是按照师傅的用java反射把,但是还是不行,不太会了,估计是哪边有点问题把。

1
2
3
4
5
6
7
8
9
10
11
12
13
function Activity4(){
var class_name = "com.example.androiddemo.Activity.FridaActivity4$InnerClasses";
var all_methods = Java.use(class_name).class.getDeclaredMethods();
console.log("all_methods => ", all_methods);
for (var i = 0; i < all_methods.length; i++) {
var method = all_methods[i];
console.log("single method => ", method);
var substring = method.toString().substr(method.toString().indexOf(class_name) + class_name.length + 1);
var finalMethodString = substring.substr(0, substring.indexOf("("));
console.log("finalMethodString => ", finalMethodString);
Java.use(class_name)[finalMethodString].implementation = function () { return true };
}
}

第六关 hook动态加载类

直接拉起下一关

image-20231008111303053

主要还是在getDynamicDexCheck

image-20231008112618340

会加载一个类作为DynamicDexCheck

image-20231008113622377

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

image-20231008113654890

那么就需要hook动态加载这个类,然后hook里面的check方法即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function five(){
Java.perform(function(){
Java.enumerateClassLoaders({
onMatch: function (loader) {
try {
if (loader.findClass("com.example.androiddemo.Dynamic.DynamicCheck")) {

Java.classFactory.loader = loader;

}
}
catch (error) {
console.log(" continuing :" + error)
}
},
onComplete: function () {
console.log("EnumerateClassloader END")
}
})
Java.use("com.example.androiddemo.Dynamic.DynamicCheck").check.implementation = function(){
return true;
}
})
}

这里的hook不是很懂,先mark

第七关

进入FridaActivity6

image-20231008113952312

hook掉对应类的check函数即可,这里总是不成功,很奇怪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function six(){
Java.perform(function(){
let Frida6Class0 = Java.use("com.example.androiddemo.Activity.Frida6.Frida6Class0");
let Frida6Class1 = Java.use("com.example.androiddemo.Activity.Frida6.Frida6Class1");
let Frida6Class2 = Java.use("com.example.androiddemo.Activity.Frida6.Frida6Class2");
Frida6Class0["check"].implementation = function(){
console.log("check called");
return true;
}
Frida6Class1["check"].implementation = function(){
return true;
}
Frida6Class2["check"].implementation = function(){
return true;
}
})
}

基础知识

参考:https://www.52pojie.cn/thread-1781093-1-1.html,这是基础知识,文件结构部分。

安卓文件结构

  1. res目录: 存放资源文件,包括icon,xml文件,目录存放资源文件,包括图片,字符串等等,APK的脸蛋由他的layout文件设计

  2. res/layout/: 存放被编译为屏幕布局(或屏幕的一部分)的XML文件

  3. 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

  4. AndroidManifest.xml文件: 应用程序配置文件,每个应用都必须定义和包含的,它描述了应用的名字、版本、权限、引用的库文件等信息。

  5. classes.dex文件: 传统 Class 文件是由一个 Java 源码文件生成的 .Class 文件,而 Android 是把所有 Class 文件进行合并优化,然后生成一个最终的 class.dex 文件。它包含 APK 的可执行代码,是分析 Android 软件时最常见的目标。由于dex文件很难看懂,可通过apktool反编译得到.smali文件,smali文件是对Dalvik虚拟机字节码的一种解释(也可以说是翻译),并非一种官方标准语言。通过对smali文件的解读可以获取源码的信息。

  6. resources.arsc文件: 二进制资源文件,包括字符串等,resources.arsc文件只包含资源的索引和映射关系,并不包含实际的资源内容。实际的资源内容存储在res文件夹中,按照资源类型和名称进行组织。当应用程序需要使用资源时,系统会根据resources.arsc文件中的索引信息找到对应的资源文件,并将其加载到内存中。

  7. smali: smali是将Android字节码用可阅读的字符串形式表现出来的一种语言,可以称之为Android字节码的反汇编语言。利用apktool或者Android Killer,反编classes.dex文件,就可以得到以smali为后缀的文件,这些smali文件就是Dalvik的寄存器语言。

  8. asssets目录:存放APK的静态资源文件,比如视频,音频,图片等

  9. lib目录:armeabi-v7a基本通用所有android设备,arm64-v8a只适用于64位的android设备,x86常见用于android模拟器,其目录下的.so文件是c或c++编译的动态链接库文件

  10. META-INF目录:保存应用的签名信息,签名信息可以验证APK文件的完整性,相当于APK的身份证(验证文件是否又被修改)

  11. 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
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
<!---声明实现应用部分可视化界面的 Activity,必须使用 AndroidManifest 中的 <activity> 元素表示所有 Activity。系统不会识别和运行任何未进行声明的Activity。----->
<activity
android:label="@string/app_name"
android:name="com.zj.wuaipojie.ui.MainActivity"
android:exported="true"> <!--当前Activity是否可以被另一个Application的组件启动:true允许被启动;false不允许被启动-->
<!---指明这个activity可以以什么样的意图(intent)启动--->
<intent-filter>
<!--表示activity作为一个什么动作启动,android.intent.action.MAIN表示作为主activity启动--->
<action
android:name="android.intent.action.MAIN" />
<!--这是action元素的额外类别信息,android.intent.category.LAUNCHER表示这个activity为当前应用程序优先级最高的Activity-->
<category
android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="com.zj.wuaipojie.ui.ChallengeFirst" />
<activity
android:name="com.zj.wuaipojie.ui.ChallengeFifth"
android:exported="true" />
<activity
android:name="com.zj.wuaipojie.ui.ChallengeFourth"
android:exported="true" />
<activity
android:name="com.zj.wuaipojie.ui.ChallengeThird"
android:exported="false" />
<activity
android:name="com.zj.wuaipojie.ui.ChallengeSecond"
android:exported="false" />
<activity
android:name="com.zj.wuaipojie.ui.AdActivity" />

所有界面都是从主要的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状态时调用状态。

105514fnc44338qubb43t3

编译过程

  1. 第一步:打包资源文件,生成R.java文件

    通过利用aapt资源打包工具,将文件目录中的Resource文件(就是工程中res中的文件)、Assets文件、AndroidManifest.xml文件、Android基础类库(Android.jar文件)进行打包,生成R.java

  2. 第二步:aidl生成Java文件

    AIDL是Android Interface Definition Language的简称, 是Android跨进程通讯的一种方式。 检索工程里所有的aidl文件,并转换为对应的Java文件

  3. 第三步:编译Java文件,生成对应的.class文件

    将R.java、aidl生成的Java文件、Java源文件通过JDK携带的Javac编译生成.class文件

  4. 第四步:把.class文件转化成Davik VM支持的.dex文件

    通过dx工具将.class文件生成为classes.dex

  5. 第五步:打包生成未签名的.apk文件

    利用apkbuilder工具,将resources.arsc、res目录、AndroidManifest.xml、assets目录、dex文件打包成未签名的apk

  6. 第六步:对未签名.apk文件进行签名

    使用apksigner为安装包添加签名信息。

  7. 第七步:对签名后的.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,修改成中文即可

image-20230919205712384

记得勾选签名

image-20230919205908259

Arsc形式

在不知道什么语言的时候,可以使用开发者助手来获取文本。

打开开发者助手,然后打开对应的应用中的未知语言所在界面,点击开发者助手小图标,找到界面资源分析,点击开始

image-20230919212921154

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

image-20230919213047388

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

image-20230919213113927

找到之后,选择翻译模式打开,进入default默认栏,然后查找修改即可。

Dex形式

搜索找到文件,使用Dex ++编辑器打开,选择字符串模式进行搜索,然后修改即可。

smali知识

可以用语法查询工具,有个apk可以安装,在安卓逆向这档事的课程三中有

image-20230920172723577

关键字

在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才对应第一个参数(因为Javastatic方法中没有this方法)

例子解析

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
//一个私有、静态、不可变的方法   方法名
.method private static final onCreate$lambda-2(Lkotlin/jvm/internal/Ref$IntRef;Lcom/zj/wuaipojie/ui/ChallengeSecond;Landroid/widget/ImageView;Landroid/widget/ImageView;Landroid/widget/ImageView;Landroid/view/View;)Z //(这里面是方法的参数)这里是方法返回值类型,表示布尔值类型,返回假或真
.registers 7 //寄存器数量

.line 33 //代码所在的行数
iget p0, p0, Lkotlin/jvm/internal/Ref$IntRef;->element:I //读取p0(第一个参数,参考寄存器知识)中element的值赋值给p0

const/4 p5, 0x1 //p5赋值1

const/16 v0, 0xa //v0赋值10,在16进制里a表示10

if-ge p0, v0, :cond_15 //判断p0的值是否大于或等于v0的值(即p0的值是否大于或等于10),如果大于或等于则跳转到:cond_15

.line 34 //以下是常见的Toast弹窗代码
check-cast p1, Landroid/content/Context; //检查Context对象引用

const-string p0, "请先获取10个硬币哦" //弹窗文本信息,把""里的字符串数据赋值给p0

check-cast p0, Ljava/lang/CharSequence; //检查CharSequence对象引用

invoke-static {p1, p0, p5}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
//将弹窗文本、显示时间等信息传给p1

move-result-object p0 //结果传递给p0

invoke-virtual {p0}, Landroid/widget/Toast;->show()V //当看到这个Toast;->show你就应该反应过来这里是弹窗代码

goto :goto_31 //跳转到:goto_31

:cond_15 //跳转的一个地址

invoke-virtual {p1}, Lcom/zj/wuaipojie/ui/ChallengeSecond;->isvip()Z //判断isvip方法的返回值是否为真(即结果是否为1)

move-result p0 //结果赋值给p0

if-eqz p0, :cond_43 //如果结果为0则跳转cond_43地址

const p0, 0x7f0d0018 //在arsc中的id索引,这个值可以进行查询

.line 37
invoke-virtual {p2, p0}, Landroid/widget/ImageView;->setImageResource(I)V //设置图片资源

const p0, 0x7f0d0008

.line 38
invoke-virtual {p3, p0}, Landroid/widget/ImageView;->setImageResource(I)V

const p0, 0x7f0d000a

.line 39
invoke-virtual {p4, p0}, Landroid/widget/ImageView;->setImageResource(I)V

.line 40
sget-object p0, Lcom/zj/wuaipojie/util/SPUtils;->INSTANCE:Lcom/zj/wuaipojie/util/SPUtils;

check-cast p1, Landroid/content/Context;

const/4 p2, 0x2 //p2赋值2

const-string p3, "level" //sp的索引

invoke-virtual {p0, p1, p3, p2}, Lcom/zj/wuaipojie/util/SPUtils;->saveInt(Landroid/content/Context;Ljava/lang/String;I)V //写入数据

goto :goto_50 //跳转地址

:cond_43

check-cast p1, Landroid/content/Context;

const-string p0, "\u8bf7\u5148\u5145\u503c\u5927\u4f1a\u5458\u54e6\uff01" //请先充值大会员哦!

check-cast p0, Ljava/lang/CharSequence;

invoke-static {p1, p0, p5}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

move-result-object p0

invoke-virtual {p0}, Landroid/widget/Toast;->show()V

:goto_50
return p5 //返回p5的值
.end method //方法结束

//判断是否是大会员的方法
.method public final isvip()Z
.registers 2

const/4 v0, 0x0 //v0赋值0

return v0 //返回v0的值

.end method




# virtual methods
.method public check(Ljava/lang/String;Ljava/lang/String;)V
.locals 2
#代表两个变量
.param p1, "name" # Ljava/lang/String;
.param p2, "pass" # Ljava/lang/String;

.line 32
const-string v0, "hfdcxy" #类似全局的字符串

#把p1和v0进行equals方法对比
invoke-virtual {p1, v0}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z

#上个函数的执行结果,即equals函数执行结果给到v0
move-result v0

#v1=0
const/4 v1, 0x0

#如果v0等于0则执行cond_0后面的语句,否则继续往下执行
if-eqz v0, :cond_0

#v0="1234"
const-string v0, "1234"

#把p2和v0进行equals对比
invoke-virtual {p2, v0}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z

move-result v0

if-eqz v0, :cond_0

.line 34
const-string v0, "\u767b\u5f55\u6210\u529f" #登录成功

invoke-static {p0, v0, v1}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

move-result-object v0

invoke-virtual {v0}, Landroid/widget/Toast;->show()V

goto :goto_0

.line 37
:cond_0
const-string v0, "\u767b\u5f55\u5931\u8d25" #登录失败

invoke-static {p0, v0, v1}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

move-result-object v0

invoke-virtual {v0}, Landroid/widget/Toast;->show()V

.line 38
:goto_0
return-void
.end method

其中unicode可以通过cyberchef进行确定

image-20230918110827822

还有一些指令

  • 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即可

    image-20230925171612027

  • DexGuard:除了ProGuard提供的功能外,还有字符串加密,花指令以及运行库防护等等。

弹窗

Android中常用的弹窗类主要有三种,分别是android.App.Dialogandroid.App.AlertDialogandroid.widget.PopupWindow,这些弹窗在打开APP运行起来之后,在内存中一般都有实例运行,那么找到这些实例即可。

HOOK抓包

基础知识

主要是网络框架的知识,现在的大多网络通信框架有如下几种

  • HttpURLConnect:原生的Android网络通信框架
  • okhttpVolleyWebSocketXMPP等:

特定框架抓包

一般情况Android的收发包相关框架为okhttpokhttp3HTTPURLConnection三种框架,那么我们需要做的就是定位这三个框架调用到的类。

1.删除~/.objection目录,即删除旧的objection.log确保后面运行得到的日志只有本次APP的内容。

2.Objection附加进程APP,输入命令android hooking list classes获取加载的所有类,退出objection

3.从objection.log中过滤出所有网络框架类okhttpokhttp3HTTPURLConnection

1
2
3
cat ./objection.log | grep -i HttpURLConnection > htpc.txt
cat ./objection.log | grep -i okhttp > okhttp.txt
cat ./objection.log | grep -i okhttp3 > okhttp3.txt

4.使用vscode补全导出的文件,Alt+Shift+鼠标向下拖动,可以选定所有行进行补全,使之成为一个objection命令

image-20230926103110130

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

image-20230926103351414

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

image-20230926103631758

7.任意选择上述被调用的函数,比如com.android.okhttp.internal.http.StreamAllocation.release,命令如下

1
2
objection -g com.cz.babySister explore
android hooking watch class_method com.android.okhttp.internal.http.StreamAllocation.release --dump-args --dump-backtrace --dump-return

随后在app中尝试登录,出现如下调用栈

image-20230926104323943

经过经验分析,可以得到最终的发包函数为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 kernelsugithub,最新的即可

image-20230928165853870

查看build number

image-20230928165919610

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

image-20230928170031221

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

image-20230928170126928

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

image-20230928165430069

release上下载img文件即可

image-20230928165503450

再关机,按住音量减和电源键进入fastboot状态,使用如下命令再刷入flash即可

1
fastboot flash boot pixel4xl_android13_4.14.276_v068.img

刷完之后,重启即可。

然后再github上找kernelsu,进入release,下载对应版本的apk进行安装即可

image-20230928165640320

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

image-20230928165727580

image-20230928165736192

红米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导出函数

image-20240119140551996

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

image-20240119142721261

有时候出错,多点两下

image-20240119143101852

sktrace

跑的比较慢,整体的trace

  • 版本:

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

image-20240119143934575

可以搜索字符串

image-20240119144241656

抓包

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

image-20231007100812480

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

image-20231007101655837

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

image-20231007101753777

image-20231007101835760

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

image-20231007102032081

最终实现抓包

image-20231007102115385

TIPS

1.AndroidKill修改签名

Androidkill原始的签名只能是V1,但是现在使用v1签名在安卓11以上无法安装,所以更改一下签名脚本AndroidRev\Tool\AndroidKiller\bin\apktool\signer.bat

更改为:

1
2
3
4
@echo off
@echo %1
@echo %2
java -jar "%~dp0\apksigner.jar" sign --ks "%~dp0\my-release-key.keystore" --ks-key-alias my-key-alias --ks-pass pass:123456 --key-pass pass:123456 --out %2 %1

这个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
2
3
PS D:\Work\AndroidRev\Tool\AndroidKiller\projects\jiaochen\Bin> adb install .\jiaochen_killer.apk
Performing Streamed Install
adb: failed to install .\jiaochen_killer.apk: Failure [-124: Failed parse during installPackageLI: Targeting R+ (version 30 and above) requires the resources.arsc of installed APKs to be stored uncompressed and aligned on a 4-byte boundary]

这时候就是4字节对问题

参考:https://blog.csdn.net/jeephao/article/details/117673542

那么就需要在签名之前用zipalign对齐一下,将zipalign.exe放入AndroidRev\Tool\AndroidKiller\bin\apktool\中,修改一下上面的脚本

1
2
3
4
5
@echo off
@echo %1
@echo %2
"%~dp0\zipalign.exe" -f -v 4 %1 %2
java -jar "%~dp0\apksigner.jar" sign --ks "%~dp0\my-release-key.keystore" --ks-key-alias my-key-alias --ks-pass pass:123456 --key-pass pass:123456 %2

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

image-20240116150842630

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

image-20240116150935826

如下所示

image-20240116151004671

在逆向时都可以的

5.NDK的so文件调试

导入IDA的android_server用root运行

image-20240116161134888

adb运行端口转发

image-20240116161152268

IDA附加调试即可

image-20240116161202324

6.JNI注册函数

静态注册函数

  • 在JAVA层如下,有static修饰的

    image-20240118160738824

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

    image-20240118160901982

动态注册函数

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

image-20240118160527165

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

image-20240117150115479

并且第一个参数均为__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
3
4
5
6
7
8
9
10
11
2.java.io.IOException: handshake failed - connection prematurally closed
at com.sun.tools.jdi.SocketTransportService.handshake(SocketTransportService.java:136)
at com.sun.tools.jdi.SocketTransportService.attach(SocketTransportService.java:232)
at com.sun.tools.jdi.GenericAttachingConnector.attach(GenericAttachingConnector.java:116)
at com.sun.tools.jdi.SocketAttachingConnector.attach(SocketAttachingConnector.java:90)
at com.sun.tools.example.debug.tty.VMConnection.attachTarget(VMConnection.java:519)
at com.sun.tools.example.debug.tty.VMConnection.open(VMConnection.java:328)
at com.sun.tools.example.debug.tty.Env.init(Env.java:63)
at com.sun.tools.example.debug.tty.TTY.main(TTY.java:1066)
致命错误:
无法附加到目标 VM。

大多是手机问题,或者存在反调试,选择低版本的手机

9.永久调试权限问题

这个问题出现在JEB4上面,换成JEB5可以了

当用MagiskHide Props Config获取永久调试权限时

image-20240119163123342

再使用JEB调试,记得还是需要修改一下AndroidManifest.xml的debuggable,不然JEB可能还是无法调试,估计是JEB分析APK时,也会检查APK中的AndroidManifest.xml的debuggabel选项,如果没设置或者不为True,那么仍然不给调试,这个可能时JEB的关系,和手机APP没什么关系。

《安卓逆向这档事》五、1000-7=?&动态调试&Log插桩 - 『移动安全区』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

10.算法助手与Android13

有时候算法助手会有这个要求

image-20240125144058321

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

image-20240125144128020

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

image-20240125144238337

参考

《教我兄弟学Android逆向系列课程+附件导航帖》 - 『移动安全区』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

《关于我在吾爱破解论坛学安卓逆向这档事》预告 - 『水漫金山』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn