谶焮 发表于 2023-3-1 13:39

从某小电影下载工具破解入手逆向实操

# 从某小电影下载工具破解入手逆向实操

**本文章中所有内容仅供学习交流使用,不用于其他任何目的,若有侵权,请联系作者立即删除!**

本次我们做一个相对入门级的教程,通过对一个安卓端下载工具(ZTQgYjggOGIgZTggYmQgYmQgZTUgYjcgYTUgZTUgODUgYjcgZTcgYWUgYjE=)的分析和逆向,把常见的简单分析方案都过一下,修改方案也都尝试一下,包括重打包、Hook等,那么废话不多说,现在就开始。

目标APP是一个下载工具,支持短视频、磁链等,但是对于非会员的下载有次数限制,非会员每天只能下载5次,本次我们的目的就是绕过非会员下载次数限制。

## 使用工具

1. jadx
2. frida
3. MT管理器
4. IDA

## jadx分析VIP逻辑

拿到APK后第一步就应该先jadx打开看一下有木有加固,然后再去分析,如果加固了就会涉及到脱壳修复,当前样本并未加固😜,故可以直接进行分析。

1. 搜索关键字

   常见的关键字如Vip、getVip,搜索后可发现如下信息
   

2. 分析getVip方法

   可以看到,搜索到的方法中有部分为Native方法,那么类似这种App就存在两种修改方案,一个是修改Native,一个是修改Java(好像是废话),从难度来讲肯定还是Java层更容易修改和调试,所以我们就先从Java层入手,先去看上面的第二个方法,getVip

   

   可以看到,此方法的调用位置很有限,仅存在一处调用,方法名为h(一处调用了三次)

3. 分析h方法
   可以看到,h方法中对于getVip的调用为Info.getVip,info为y2.a().e()的返回值,可以查看一下此返回值是什么
   

4. Hooke方法,打印info的结果如下

   

   ```js
   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. 分析到现在,就可以开始思考修改思路了

## 修改思路

从上面的简单分析就可以看出,应用对于VIP的判断是基于info中的vip字段,则可以通过修改info中的vip字段来实现绕过vip,也可以通过修改非会员每日下载次数来实现下载次数绕过,两种方式都可以实现无限制下载的目的。

所以就可以从以下几个角度来进行修改:

1. 那么我们就可以通过修改UserInfo对象或其返回值,使其vip信息为11-13中的一个

2. 修改h方法的smail,修改第一个if
   可以修改第一个if判断,将`info.getVip==11`修改为`info.getVip==0`如此就可以实现非会员走会员逻辑(0为非会员,数值可以通过Hook对应方法发现)
3. 向APP注入frida-gadget,同上一个方案一样修改UserInfo实现获取VIP
4. 修改todayCount的值,使其一直为0
   从代码中可以看到,客户端会判断todayCount的值是否小于count,那么可以认为count就是非会员每日下载限额,可以试count变大或者todayCount变小也可以绕过下载次数限制,而不需要修改会员身份。
5. 修改h方法调用位置,将其参数中的count改为更大的值
   例如在h方法被调用时,将其实参固定为9999

以上几个思路,每个思路都可以通过重打包或者Hook来实现,下面我们就开始实践一下,主要针对前三种方法,其他两种有兴趣的小伙伴可自行尝试

## Hook(第一种方案)

1. Hook e方法,打印返回值

   ```js
   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;
   };
   ```

2. 修改返回值

   修改UserInfo的返回值,步骤就是先将UserInfo序列化为JSON,修改后再用Gson反序列化为UserInfo对象,frida脚本如下

   ```js
   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;
   };
   ```

3. 验证结果
   执行后可以发现,已经是永久会员了
   
   为了防止仅仅是修改了显示实际仍然是非会员,所以需要再尝试一下解析次数,也可以发现非会员的解析次数限制也木有了

## 重打包(第二种方案)

1. MT管理器解包找到对应的方法
2. 修改smail并保存,在h方法中看到getVip调用的位置,将其中的一个0xb或0xc修改为0x0即可

3. 反编译为java看看修改效果
4. 重新打包安装
5. 验证修改结果
6. 另外需要注意,if中有两个判断,一个是userinfo 不为空,一个是是否为VIP,此修改方法仅改了VIP状态,所以还是需要登录的,如果想不登录使用可以尝试修改第一个判断,eqz修改为nez

不过目前Apk重打包目前存在一些问题,APP内置了一个native-security,会校验客户端的完整性,若被篡改,则会强制要求更新,绕过方法可以是配合httpcanary之类的抓包工具将`api/security/upload`这个接口的请求丢掉就可以继续走下去,但是此方法需要额外操作且对设备有一定的要求,所以不太够通用,我们继续看第三种方案。

## 重打包(第三种方案)

若想实现在非Root设备上直接使用,方案1和方案2都存在一定的问题,所以我们现在再来看下方案三:通过注入frida-gadget.so来实现在非Root设备上运行,此方法和方案二一样需要过掉APP的完整性验证方法,不过执行起来会相对简单一些(最起码不需要去改汇编😛,使用时也无需多余操作)

### 完整性校验分析(IDA)

1. 在进行第二种方案时我们发现,App具备完整性验证,重新打包APP后会出现APP退出并跳转至更新网址的情况,所以我们需要继续分析其校验逻辑

2. 通过抓包,我们可以看到,是收到了api/security/upload的返回值之后才弹出的错误提示,那么可以先去搜索一下请求的url、更新网址或者host

3. 经过搜索,在jadx中并未搜索到对应的host或url信息,那么我们就可以怀疑是native层进行的校验


4. 现在我们可以直接找一下security相关的代码,也可以直接HookSystem.loadLibrary看看是加载到哪个so时APP退出,也可以直接去lib看一下有没有相关的so,于是就可以找到libnative-security.so

   ```js
   //
   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);
   }
   };
   ```


5. 之后找到so的加载位置在SecurityJNI.Java中

6. 然后我们发现其中有一个方法叫做`nativeInit(Context context);`,追踪这个方法的调用,然后就看到了这个方法,通过Hook调试等方法确定了是nativeInit之后APP才退出的,于是锁定此方法,继续分析

   ```java
   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();
   }
   }
   ```

7. 分析一下native-security中的nativeInit,IDA伪代码如下

   ```cpp
   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<decltype(nullptr)>();
   v15 = (char *)sub_F63F0((__int64)v18);
   v2 = (char *)ping(a1, v15);
   if ( ((unsigned __int8)v2 & 1) != 0 )
   {
   v3 = "adverts.indabai.com";
   v2 = v18;
   }
   ```

8. 一眼看过来,sub_F63F0应该是我们的目标,之前我们已经分析出了upload接口是校验完整性的,并且在nativeInit中并未发现对应的更新地址,且ping函数的参数中有sub_F63F0返回的V15,有可能就是接收的更新地址

9. 直接Hook sub_F63F0

   ```js
   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=' + hexdump(args));
   },
   onLeave: function (retval) {
       console.log('sub_F63F0 onLeave' + hexdump(ret);
                   }
   });
   ```

10. 可以看到如下信息


11. 修改返回值

    ```js
    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=' + hexdump(args));
      },
      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)
      }
    });
    ```

12. 正常运行


### 开始重打包

分析完完整性验证方法后,我们就可以正式开始修改工作了

1. 下载frida-gadget.so,选一个适合自己的版本,(https://github.com/frida/frida/releases),若出现在部分设备上可用部分设备不可用的情况,则可以更换一下gadget版本,目前遇到过14版本的gadget在Android12不可用的情况

2. 编写脚本,增加js脚本将方案1的UserInfo修改脚本和绕过完整性校验脚本写入文件(eg:命名为DDD.js)

3. 编写配置文件(配置文件名称需要和frida-gadget.so的名字相同,如frida-gadget.so的配置文件名称应该为frida-gadget.config.so)

   ```json
   {
   "interaction": {
       "type": "script",
       "path": "/sdcard/Download/Script/DDD.js",
       "on_change":"reload"
   }
   }
   ```

4. 将frida-gadget.so和frida-gadget.config.so放入lib目录对应文件夹,64位的就放arm64_v8a,可以用MT管理器操作,直接增加进去即可

5. 挑选合适的时机加载frida-gadget.so,加载的早了,可能security.so还没加载,会导致Hook失败,加载的晚了,nativeInit执行完了,也会失败,所以我们选择在security.so load完成之后立即load frida-gadget.so

6. MT管理器打开apk找到,在` System.loadLibrary("native-security");`对应的smail代码下增加如下内容

   ```java
   const-string v0, "frida-gadget" invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
   ```

7. 将DDD.js文件放到配置文件中配置的位置

8. 打包、签名、安装、给读写外置存储权限、启动

9. 进入APP后注册、登录,若显示为永久会员则说明已成功

## 最后

本帖仅用于交流技术随机选择APP进行分析,但是这个工具箱我个人用下来还非常好用,可以下载几十种类型的链接,基本上包含了所有短视频,价格也很便宜,二十来块就可以买永久会员,建议大家去支持一下&#128539;。

我为52pojie狂 发表于 2023-3-2 11:31

小电影是什么意思?是两三个人一个场景的那种人体艺术片吗?

佚名RJ 发表于 2023-3-2 21:10

这个软件直接逆向是很好破解的,但是就是检验安装包完整性,直接跳转到浏览器强制更新下载,每个版本改的都是这样。具体软件叫什么,从第一种方案的图片用过的,一般都看出来了

zy3333351 发表于 2023-3-2 14:56

学习了。谢谢分享!!

Itmedo 发表于 2023-3-3 12:01

base64解码之后是汉字的utf8编码,对照解码即可

huduke 发表于 2023-3-2 11:08

谢谢分享,有没有ios app破解实操。

adolphin 发表于 2023-3-2 11:28

可以啊,这样子就ok了

fangxiaolong 发表于 2023-3-2 11:57

谢谢分享

yboopp 发表于 2023-3-2 11:59

先码住   进收藏吃灰吧   {:1_886:}

破凤凰 发表于 2023-3-2 12:25

感谢老师无私分享。

bpzm1987 发表于 2023-3-2 13:10

厉害,加油,前来学习下!

Kingloo 发表于 2023-3-2 13:41

看到getVIP字樣了,感覺是很厲害的工具
页: [1] 2 3 4 5 6 7 8 9 10
查看完整版本: 从某小电影下载工具破解入手逆向实操