首页
论坛
课程
招聘
[原创]使用frida/xposed对某灰色APP进行暴力破解
2021-7-30 15:38 12711

[原创]使用frida/xposed对某灰色APP进行暴力破解

2021-7-30 15:38
12711

使用frida/xposed对某灰色APP进行暴力破解

近日,发现了一款视频APP,里面的内容极其不堪入目,且需要收费才能观看,否则每天(不确定,也可能是几小时)只有1次免费观看机会

 

于是对其进行了暴力破解(不知道怎么回事,自从破了之后就没怎么打开过这玩意儿了...)

 

大概前两天的时候,这个APP的大版本号升级到了5.0,幸运的是,破解方法照样有效,趁此机会记录一下Android逆向小白的心路历程

 

 

在打开APP的时候就会根据手机特征码帮你自动注册一个账号,名字和头像都是随机的,好像也没法退出和切换账号?感觉换台设备你充的钱就没了

 

 

该APP中有2种视频,一种是可以使用免费观看次数观看的免费视频,一种是收费的,收费视频意味着不仅要开通VIP,还得额外交钱才能看

 

名字下面有个免费观看次数,随便点开一个免费视频后就从1变成0了,不知道过了多久,它又会变成1,若免费观看次数为0,则点开视频会提示次数用尽无法播放

 

对APP进行测试,发现使用免费观看次数观看过的视频可以重复观看,关闭应用再重启也能观看,但是清除数据后再次打开APP,虽然账号还是那个账号,免费观看次数依旧为0,但刚刚的视频却不能观看了,由此推断某些数据应该是存在本地的,并且有代码对该部分进行检验,从而决定能否播放视频

 

第一时间想到的是看看能不能直接用MT管理器修改一下dex绕过这个判断,结果一看,某加固,不好搞,先另辟蹊径试试

 

 

首先在没有观看视频的情况下打开/data/data/com.tencent.mm.oneff/shared_prefs目录,可以看到一个SharedPreferencesData_tbr.xml文件

 

 

然后随便点开一个视频,再查看该文件的内容

 

 

可以看到LOCAL_VD_HI_KEY中保存了刚才点开的视频的信息,推测401979是视频的ID

 

将401979:{......}改成401979:{},保存后重新打开APP,发现仍然可以观看该视频,将整个map项删掉,则不能观看了,再次恢复map的内容,又能观看了,由此推断内部写法应该是if(xxx.containsKey(ID))这种形式

 

当时的想法是,给它把0到999999全部填上去,应该就可以了吧?实验证明,填的项目数量超过8万后APP会崩,只填8万个再去测试发现确实可以观看少部分视频,但毕竟区间覆盖不广,这种办法pass

 

看来只能正面进攻了,上frida!

 

main.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import sys
 
import frida
 
import scripts
 
 
def on_message(message, data):
    if message['type'] == 'send':
        print(message['payload'])
    else:
        print(message)
 
 
app = 'com.tencent.mm.oneff'
 
dev = frida.get_remote_device()
pid = dev.spawn(app)
proc = dev.attach(pid)
script = proc.create_script(scripts.dex_dump % app)
script.on('message', on_message)
script.load()
dev.resume(app)
sys.stdin.read()

scripts.py

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
# %s: App package name
dex_dump = '''
var dex_count = 0
Interceptor.attach(
    Module.findExportByName(
        'libart.so',
        '_ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPNS_6MemMapEPKNS_10OatDexFileEPS9_'
    ),
    {
        onEnter: function (args) {
            var begin = args[1]
            var address = parseInt(begin, 16) + 0x20
            var dex_size = Memory.readInt(ptr(address))
            dex_count++
            send('Dex' + dex_count + ' Size : ' + dex_size)
            var file = new File('/data/data/%s/classes' + (dex_count == 1 ? '' : dex_count) + '.dex', 'wb')
            file.write(Memory.readByteArray(begin, dex_size))
            file.flush()
            file.close()
        },
        onLeave: function (retval) {
        }
    }
);
'''

 

 

用脱壳脚本将dex给dump了下来,但是发现APP会一直卡在启动页面,下掉脱壳脚本照样卡住,推测有某些反制手段

 

用frida attach APP会瞬间闪退,而直接spawn会一直卡住,即使改frida-server的名字、端口也无效,于是想到了xposed,写了一个测试模块,惊喜的发现可以绕过APP的防护,这下好整了

 

使用MT管理器的Dex转Jar功能,把几个dex都转成jar,然后解压到电脑上(因为Windows不分大小写,而经过混淆的代码中有许多类似A、a的类文件,所以解压时不要选择覆盖,而是选择重命名),使用IDEA打开

 

 

通过字符串定位到com.ss.android.article.uitls.Aa这个类

 

 

在这个APP中,很多类都有一个静态方法来获取该类的实例,几乎看到类名.方法名().xxx就知道是访问某个类的实例对象了

 

 

Aa这个类中e方法是返回LOCAL_VD_HI_KEY的内容

 

接着找到了对应的map项的bean类

 

com.ss.android.article.listplayer.adapter.ListLikeVideoBean.class

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
public class ListLikeVideoBean extends ListPlayerBaseBean {
    public int club_id;
    public int coins;
    public int count_comment;
    public int count_like;
    public String count_like_str;
    public int count_pay;
    public int count_play;
    public String count_play_str;
    public String created_at;
    public String created_date;
    public String desc;
    public int duration;
    public String duration_str;
    public int id;
    public boolean isChecked = false;
    public boolean isChoice = false;
    public boolean isEdit = false;
    public boolean isInit = true;
    public boolean isLike;
    public int isTop;
    public int is_activity;
    public boolean is_club;
    public int is_origin;
    public boolean is_pay;
    public int isfree;
    public ListLikeVideoBean.MemberBean member;
    public int mv_type;
    public String refresh_at;
    public String reject_reason;
    public String source_1080;
    public String source_240;
    public String source_480;
    public String source_720;
    public int status;
    public String status_str;
    public List<String> tags;
    public String thumb_cover;
    public int thumb_height;
    public int thumb_width;
    public String title;
    public int v_ext;
 
    public ListLikeVideoBean() {
    }
 
    public int getItemType() {
        return super.itemType;
    }
 
    public static class MemberBean implements Serializable {
        public boolean auth_status;
        public int fans_count;
        public int followed_count;
        public boolean is_follow;
        public String nickname;
        public String phone;
        public String taggroup_name;
        public String thumb;
        public String username;
        public String uuid;
        public int videos_count;
 
        public MemberBean() {
        }
    }
}

紧接着在com.ss.android.article.listplayer.F这个类中找到了对上述bean的引用

 

 

其中读入数据的地方可以很明显的看见

 

 

 

而最底层的判断函数则是下面的f函数

 

 

该函数对当前的视频进行判断,is_pay==true表示这是个收费视频并且你已经购买了,e.containsKey(c.id)和我之前猜测的一模一样,isfree==1表示这是个免费视频

 

所以我们只需要hook该函数并始终返回true即可

 

题外话:如果hook下图的d函数并返回99999,你就能在个人页面看见你有99999次免费观看次数了

 

 

下面开始编写xposed模块

 

打开Android Studio并创建一个No Activity的项目

 

 

我创建的包名是com.titvt.xposed

 

接着打开AndroidManifest.xml,在Application标签内添加下面的内容

1
2
3
4
5
6
7
8
<application
    省略... >
 
    <meta-data android:name="xposedmodule" android:value="true" />
    <meta-data android:name="xposeddescription" android:value="xposed" />
    <meta-data android:name="xposedminversion" android:value="53" />
 
</application>

这表示你写的是xposed模块,并且能被xposed框架识别到

 

然后在AndroidManifest.xml的同级目录下新建assets目录,在assets下新建xposed_init文件(类型是文本文件),在文件中写下你的模块要用到的类(每行一个类,比如我只有一个叫Xposed的类,我就写com.titvt.xposed.Xposed)

 

别忘了在build.gradle(:app)中添加依赖,一定要用compileOnly

1
2
3
4
5
6
dependencies {
 
    省略...
 
    compileOnly 'de.robv.android.xposed:api:82'
}

做好准备工作后,开始正式的编写模块

 

新建一个名为Xposed的类文件,定义一个名为Xposed的类,实现IXposedHookLoadPackage接口

 

实现该接口后需要重写handleLoadPackage函数,该函数会在每个package被加载的时候被调用,我们可以通过它的参数来获取以及修改当前package的信息、hook各种method

 

可以通过packageName来判断当前加载的package是否是我们期望的APP

 

由于加壳的缘故,直接hook是拿不到target的,因此先要hook壳程序来拿到被壳手动加载的部分

 

 

查询资料后发现可以从attachBaseContext函数的Context参数中拿到class loader,进而hook真正的内容,对应到本APP就是com.SecShell.SecShell.AW.attachBaseContext()

 

剩下的具体的就不细讲了,网上有大量的教程,下面放出完整的代码

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
package com.titvt.xposed;
 
import android.content.Context;
 
import java.lang.reflect.Method;
 
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;
 
public class Xposed implements IXposedHookLoadPackage {
    @Override
    public void handleLoadPackage(LoadPackageParam lpparam) throws Throwable {
        XposedBridge.log(lpparam.packageName);
        if (!lpparam.packageName.equals("com.tencent.mm.oneff")) {
            return;
        }
        Class<?> AW = null;
        try {
            AW = XposedHelpers.findClass("com.SecShell.SecShell.AW", lpparam.classLoader);
        } catch (Exception ignored) {
        }
        if (AW == null) {
            XposedBridge.log("Find AW fail");
            return;
        }
        Method attachBaseContext = null;
        try {
            attachBaseContext = XposedHelpers.findMethodExact(AW, "attachBaseContext", Context.class);
        } catch (Exception ignored) {
        }
        if (attachBaseContext == null) {
            XposedBridge.log("Find attachBaseContext fail");
            return;
        }
        XposedHelpers.findAndHookMethod(AW, "attachBaseContext", Context.class, new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                XposedBridge.log("Hook attachBaseContext succeed");
                ClassLoader classLoader = ((Context) param.args[0]).getClassLoader();
                Class<?> F = null;
                try {
                    F = XposedHelpers.findClass("com.ss.android.article.listplayer.F", classLoader);
                } catch (Exception ignored) {
                }
                if (F == null) {
                    XposedBridge.log("Find F fail");
                    return;
                }
                Method f = null;
                try {
                    f = XposedHelpers.findMethodExact(F, "f");
                } catch (Exception ignored) {
                }
                if (f == null) {
                    XposedBridge.log("Find f fail");
                    return;
                }
                XposedHelpers.findAndHookMethod(F, "f", new XC_MethodHook() {
                    @Override
                    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                        XposedBridge.log("Hook f succeed");
                        param.setResult(true);
                    }
                });
            }
        });
    }
}

然后编译Release版APK,安装启动一气呵成,现在所有的免费视频都任君随便看啦(收费视频也可以点开,但是视频画面内容是“该版本已停止维护...”,推测是服务器不给播,于是返回回来的视频链接是这种提示画面)

 

总结一下,该APP防护做得比较到位,加壳、混淆、m3u8视频还配上了sign,甚至还有APP低版本检测,唉,驾照难考啊= =


[注意] 欢迎加入看雪团队!base上海,招聘CTF安全工程师,将兴趣和工作融合在一起!看雪20年安全圈的口碑,助你快速成长!

收藏
点赞5
打赏
分享
最新回复 (12)
雪    币: 2135
活跃值: 活跃值 (2178)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
YenKoc 活跃值 2021-7-30 16:38
2
0
这啥梆梆加固,免费版的吧,整体脱壳还有指令没抽取的吗,正常梆梆,frida和xposed都用不了
雪    币: 1305
活跃值: 活跃值 (413)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
幼稚园小朋友 活跃值 2021-7-30 17:38
3
0
雪    币: 1521
活跃值: 活跃值 (3563)
能力值: ( LV3,RANK:25 )
在线值:
发帖
回帖
粉丝
supperlitt 活跃值 2021-7-31 16:39
4
1
。。。。。“自从破了之后就没怎么打开过这玩意儿了”,这我是一W个不信。
雪    币: 591
活跃值: 活跃值 (387)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
网络枭红 活跃值 2021-8-2 10:47
5
0
自从破了之后就没怎么打开过这玩意儿了。此地无银!
雪    币: 962
活跃值: 活跃值 (409)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
只是来打酱油 活跃值 2021-8-4 10:28
6
0
我相信楼主破解之后再也没看
雪    币: 962
活跃值: 活跃值 (409)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
只是来打酱油 活跃值 2021-8-4 10:28
7
0
有没有办法不root直接hoook呢
雪    币: 229
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
lzonel 活跃值 2021-8-5 09:01
8
0
我有个朋友
雪    币: 231
活跃值: 活跃值 (239)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
执念成狂 活跃值 2021-8-5 10:46
9
0
这个直接分析接口下载可能更方便一点,就是简单的AES加密。金币的视频接口不直接返回播放链接,有二次校验。免费视频也很多,诸位小心肾虚
雪    币: 26
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
万里星河 活跃值 2021-8-5 20:32
10
0
支持一下
雪    币: 38
活跃值: 活跃值 (41)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
nrj 活跃值 2021-8-8 23:22
11
0
这个好复杂啊 算了 我还是多建几个虚拟机 分享了  分享一个阔以白嫖3天
雪    币: 4
活跃值: 活跃值 (431)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
wx_范迪塞尔 活跃值 2021-8-9 16:54
12
0
那么问题来了,apk下载在哪呢
雪    币: 2282
活跃值: 活跃值 (294)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
biggodness 活跃值 2021-8-9 22:33
13
0
同求apk
游客
登录 | 注册 方可回帖
返回