本文章中所有内容仅供学习交流使用,不用于其他任何目的,若有侵权,请联系作者立即删除!
本次我们做一个相对入门级的教程,通过对一个安卓端下载工具(ZTQgYjggOGIgZTggYmQgYmQgZTUgYjcgYTUgZTUgODUgYjcgZTcgYWUgYjE=)的分析和逆向,把常见的简单分析方案都过一下,修改方案也都尝试一下,包括重打包、Hook等,那么废话不多说,现在就开始。
目标APP是一个下载工具,支持短视频、磁链等,但是对于非会员的下载有次数限制,非会员每天只能下载5次,本次我们的目的就是绕过非会员下载次数限制。
使用工具
[ol]
[/ol]
jadx分析VIP逻辑
拿到APK后第一步就应该先jadx打开看一下有木有加固,然后再去分析,如果加固了就会涉及到脱壳修复,当前样本并未加固,故可以直接进行分析。
[ol]
搜索关键字
常见的关键字如Vip、getVip,搜索后可发现如下信息
1.png (138.18 KB, 下载次数: 0)
下载附件
搜索结果
2023-3-1 13:33 上传
分析getVip方法
可以看到,搜索到的方法中有部分为Native方法,那么类似这种App就存在两种修改方案,一个是修改Native,一个是修改Java(好像是废话),从难度来讲肯定还是Java层更容易修改和调试,所以我们就先从Java层入手,先去看上面的第二个方法,getVip
2.png (63.22 KB, 下载次数: 0)
下载附件
分析结果
2023-3-1 13:33 上传
可以看到,此方法的调用位置很有限,仅存在一处调用,方法名为h(一处调用了三次)
分析h方法
可以看到,h方法中对于getVip的调用为Info.getVip,info为y2.a().e()的返回值,可以查看一下此返回值是什么
3.png (131.21 KB, 下载次数: 0)
下载附件
目标方法
2023-3-1 13:33 上传
Hooke方法,打印info的结果如下
4.png (37.77 KB, 下载次数: 0)
下载附件
UserInfo
2023-3-1 13:33 上传
let C4836y2 = Java.use("g.d0.a.n.g.y2");
C4836y2["e"].implementation = function () {
console.log('e is called');
let ret = this.e();
let mjson = Gson.$new().toJson(ret);
console.log('e ret value is ' + mjson2);
return ret2;
};
5.png (138.74 KB, 下载次数: 0)
下载附件
返回信息
2023-3-1 13:33 上传
分析到现在,就可以开始思考修改思路了
[/ol]
修改思路
从上面的简单分析就可以看出,应用对于VIP的判断是基于info中的vip字段,则可以通过修改info中的vip字段来实现绕过vip,也可以通过修改非会员每日下载次数来实现下载次数绕过,两种方式都可以实现无限制下载的目的。
所以就可以从以下几个角度来进行修改:
[ol]
那么我们就可以通过修改UserInfo对象或其返回值,使其vip信息为11-13中的一个
修改h方法的smail,修改第一个if
可以修改第一个if判断,将info.getVip==11修改为info.getVip==0如此就可以实现非会员走会员逻辑(0为非会员,数值可以通过Hook对应方法发现)
向APP注入frida-gadget,同上一个方案一样修改UserInfo实现获取VIP
修改todayCount的值,使其一直为0
从代码中可以看到,客户端会判断todayCount的值是否小于count,那么可以认为count就是非会员每日下载限额,可以试count变大或者todayCount变小也可以绕过下载次数限制,而不需要修改会员身份。
修改h方法调用位置,将其参数中的count改为更大的值
例如在h方法被调用时,将其实参固定为9999
[/ol]
以上几个思路,每个思路都可以通过重打包或者Hook来实现,下面我们就开始实践一下,主要针对前三种方法,其他两种有兴趣的小伙伴可自行尝试
Hook(第一种方案)
[ol]
Hook e方法,打印返回值
let C4836y2 = Java.use("g.d0.a.n.g.y2");
C4836y2["e"].implementation = function () {
console.log('e is called');
let ret = this.e();
let mjson = Gson.$new().toJson(ret);
console.log('e ret value is ' + mjson2);
return ret2;
};
修改返回值
修改UserInfo的返回值,步骤就是先将UserInfo序列化为JSON,修改后再用Gson反序列化为UserInfo对象,frida脚本如下
let Gson = Java.use("com.google.gson.Gson");
let Cy2 = Java.use("g.d0.a.n.g.y2");
y2["e"].implementation = function () {
console.log('e is called');
let ret = this.e();
let mjson = Gson.$new().toJson(ret);
let mjson2 = JSON.parse(mjson);
mjson2["vip"] = 11;
mjson2["vipTime"] = "2999-01-01 08:00:00";
mjson2["vipType"] = "永久会员";
let ret2 = Gson.$new().fromJson(JSON.stringify(mjson2),Java.use("xxx.UserInfo").class)
console.log('e ret value is ' + ret2);
return ret2;
};
验证结果
执行后可以发现,已经是永久会员了
6.png (66.54 KB, 下载次数: 0)
下载附件
验证结果
2023-3-1 13:33 上传
为了防止仅仅是修改了显示实际仍然是非会员,所以需要再尝试一下解析次数,也可以发现非会员的解析次数限制也木有了
[/ol]
重打包(第二种方案)
[ol]
7.png (122.6 KB, 下载次数: 0)
下载附件
修改校验值
2023-3-1 13:33 上传
[/ol]
不过目前Apk重打包目前存在一些问题,APP内置了一个native-security,会校验客户端的完整性,若被篡改,则会强制要求更新,绕过方法可以是配合httpcanary之类的抓包工具将api/security/upload这个接口的请求丢掉就可以继续走下去,但是此方法需要额外操作且对设备有一定的要求,所以不太够通用,我们继续看第三种方案。
重打包(第三种方案)
若想实现在非Root设备上直接使用,方案1和方案2都存在一定的问题,所以我们现在再来看下方案三:通过注入frida-gadget.so来实现在非Root设备上运行,此方法和方案二一样需要过掉APP的完整性验证方法,不过执行起来会相对简单一些(最起码不需要去改汇编,使用时也无需多余操作)
完整性校验分析(IDA)
[ol]
在进行第二种方案时我们发现,App具备完整性验证,重新打包APP后会出现APP退出并跳转至更新网址的情况,所以我们需要继续分析其校验逻辑
通过抓包,我们可以看到,是收到了api/security/upload的返回值之后才弹出的错误提示,那么可以先去搜索一下请求的url、更新网址或者host
经过搜索,在jadx中并未搜索到对应的host或url信息,那么我们就可以怀疑是native层进行的校验
[/ol]
8.png (57.81 KB, 下载次数: 0)
下载附件
upload搜索结果
2023-3-1 13:33 上传
现在我们可以直接找一下security相关的代码,也可以直接Hook System.loadLibrary看看是加载到哪个so时APP退出,也可以直接去lib看一下有没有相关的so,于是就可以找到libnative-security.so
//
let System = Java.use('java.lang.System');
let Runtime = Java.use('java.lang.Runtime');
let SystemLoad_2 = System.loadLibrary.overload('java.lang.String');
let VMStack = Java.use('dalvik.system.VMStack');
SystemLoad_2.implementation = function(library) {
console.log("Loading library =====> " + library);
try {
let loaded = Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), library);
return loaded;
} catch(ex) {
console.log(ex);
}
};
[/ol]
9.png (172.71 KB, 下载次数: 0)
下载附件
加载so
2023-3-1 13:33 上传
之后找到so的加载位置在SecurityJNI.Java中
然后我们发现其中有一个方法叫做nativeInit(Context context);,追踪这个方法的调用,然后就看到了这个方法,通过Hook调试等方法确定了是nativeInit之后APP才退出的,于是锁定此方法,继续分析
public void f(Context context, g.d0.c.d.a call) {
this.b = context;
if (this.a) {
if (call != null) {
call.a();
return;
}
return;
}
this.a = true;
SecurityJNI.nativeInit(context);
g.d0.c.g.a.a("init finish");
e.g();
if (call != null) {
call.a();
}
}
分析一下native-security中的nativeInit,IDA伪代码如下
char *v15;
v19 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
__android_log_print(6, "SecurityJNI", "checkApk %s", "before");
std::string::basic_string();
v15 = (char *)sub_F63F0((__int64)v18);
v2 = (char *)ping(a1, v15);
if ( ((unsigned __int8)v2 & 1) != 0 )
{
v3 = "adverts.indabai.com";
v2 = v18;
}
一眼看过来,sub_F63F0应该是我们的目标,之前我们已经分析出了upload接口是校验完整性的,并且在nativeInit中并未发现对应的更新地址,且ping函数的参数中有sub_F63F0返回的V15,有可能就是接收的更新地址
直接Hook sub_F63F0
let sub_F63F0 = get_func_addr("libnative-security.so",0xF63F0);
console.log('sub_F63F0=' + sub_F63F0);
Interceptor.attach(sub_F63F0, {
onEnter: function (args) {
console.log('onEnter');
console.log('sub_F63F0 args[a1]=' + hexdump(args[0]));
},
onLeave: function (retval) {
console.log('sub_F63F0 onLeave' + hexdump(ret);
}
});
可以看到如下信息
[/ol]
10.png (716.72 KB, 下载次数: 0)
下载附件
打印返回值
2023-3-1 13:33 上传
修改返回值
let sub_F63F0 = get_func_addr("libnative-security.so",0xF63F0);
console.log('sub_F63F0=' + sub_F63F0);
Interceptor.attach(sub_F63F0, {
onEnter: function (args) {
console.log('onEnter');
console.log('sub_F63F0 args[a1]=' + hexdump(args[0]));
},
onLeave: function (retval) {
let ret = Memory.readCString(retval);
if(ret.indexOf("isCrack") != -1){
console.log('===============');
ret = ret.replace(true,false)
}
console.log('sub_F63F0 onLeave' + ret);
Memory.writeUtf8String(retval,ret)
}
});
正常运行
[/ol]
11.png (396.47 KB, 下载次数: 0)
下载附件
修改后的返回值
2023-3-1 13:34 上传
开始重打包
分析完完整性验证方法后,我们就可以正式开始修改工作了
[ol]
下载frida-gadget.so,选一个适合自己的版本,Releases · frida/frida (github.com),若出现在部分设备上可用部分设备不可用的情况,则可以更换一下gadget版本,目前遇到过14版本的gadget在Android12不可用的情况
编写脚本,增加js脚本将方案1的UserInfo修改脚本和绕过完整性校验脚本写入文件(eg:命名为DDD.js)
编写配置文件(配置文件名称需要和frida-gadget.so的名字相同,如frida-gadget.so的配置文件名称应该为frida-gadget.config.so)
{
"interaction": {
"type": "script",
"path": "/sdcard/Download/Script/DDD.js",
"on_change":"reload"
}
}
将frida-gadget.so和frida-gadget.config.so放入lib目录对应文件夹,64位的就放arm64_v8a,可以用MT管理器操作,直接增加进去即可
挑选合适的时机加载frida-gadget.so,加载的早了,可能security.so还没加载,会导致Hook失败,加载的晚了,nativeInit执行完了,也会失败,所以我们选择在security.so load完成之后立即load frida-gadget.so
MT管理器打开apk找到,在System.loadLibrary("native-security");对应的smail代码下增加如下内容
const-string v0, "frida-gadget" invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
将DDD.js文件放到配置文件中配置的位置
打包、签名、安装、给读写外置存储权限、启动
进入APP后注册、登录,若显示为永久会员则说明已成功
[/ol]
最后
本帖仅用于交流技术随机选择APP进行分析,但是这个工具箱我个人用下来还非常好用,可以下载几十种类型的链接,基本上包含了所有短视频,价格也很便宜,二十来块就可以买永久会员,建议大家去支持一下。