首页
论坛
课程
招聘
[原创]某视频app(V15.7-V18.4)的学习记录
2021-12-1 14:49 27770

[原创]某视频app(V15.7-V18.4)的学习记录

2021-12-1 14:49
27770

[原创]某视频app(V15.7-V18.4)的学习记录



        之前发过一个帖子:    某视频app(V15.7)及web分析记录https://bbs.pediy.com/thread-269480.htm).

        不知不觉版本都到18+了,记录下学习过程,技术交流用。



一、软硬件环境:

 

抖音android (应用宝渠道)

IDA 7.5

Frida 14.2.2

Gda3.86

JEB

jadx-gui

unidbg

LineageOs 17.1 (android 10)

小米8

 

二、流水账 (直接按时间顺序,有坑写坑)


1. 截包相关    

    这个看见有人说有个版本开始不能截了,我这边一直都是换证书的,没感觉有影响,估计我下的是盗版,碰到再看了。


2. 之前看到so都是32位的,后来都换成64位的了。

3. 网络请求相关


          之前就看到有使用Cronet模块,想看下实现过程,下过源码(https://github.com/hanpfei/chromium-net),一个是发现跟app使用的并不完全相同,

          再一个还要自己编译模块,就想到直接使用抖音的模块。

          先新建一个自己的测试app,接着的工作就是搬代码了,直接导出所有的反编译代码,结合源码,首先就是这个包:

    com.ttnet.org.chromium.net

        


        这里插下搬代码中的一些问题:

        首先就是反编译代码,几个工具(jeb ,jadx, GDA)各有优劣,要结合使用, jadx得到的代码可读性更好,但是很多函数识别不出来,比如这种:


GDA擅长处理疑难杂症,其它2个识别不了的代码,它这个都能识别,不过可读性不好,基本不能直接编译通过,需要看清楚逻辑后修复。


再就是反编译代码中变量顺序问题,也会影响编译(初始化的变量放在后面了):


代码修复中有些类型的转换,比如Bundle类型变量为null的,可能被还原成了int i=0; 然后条件判断的时候,这个也会出现编译错误。

再就是Map.Entry这种,需要先用object遍历,再转换类型。

if (!StringsKt.contains$default((CharSequence) name, (CharSequence) "__MACOSX", false, 2, (Object) null)) {

kotlin相关也要修改

StringsKt.contains((CharSequence) str, (CharSequence) "?", false))


还有下面这种继承的

public final class FrontMethodFragment$onCreateFailed$1 extends Lambda implements Function0<Unit> {

Lambda要带上Lambda<Unit>

参考这个 cannot be inherited with different type arguments_PhilsphyPrgram的博客-CSDN博客


还有一些逻辑上的,少了break之类,就不是很好发现了,这个只有遇到后调试修复了。

最后还有麻烦的就是缺少异常处理,java编译是强制要求处理异常的,找过资料,想编译时候忽略异常处理,发现是JAVA规范强制的,屏蔽不了,加这个也花了不少时间。


总之,3个工具要结合使用,怎么方便怎么来。


搬过来后,直接调用测试:

                // 初始化引擎
                CronetEngine.Builder myBuilder = new CronetEngine.Builder(getApplicationContext());
                CronetEngine cronetEngine = myBuilder.build();

                // 创建请求线程
                Executor executor = Executors.newSingleThreadExecutor();

                // 创建UrlRequest
                strUrl="http://10.0.0.217/about";
                UrlRequest.Builder requestBuilder = cronetEngine.newUrlRequestBuilder(
                        strUrl, new MyUrlRequestCallback(), executor);
                requestBuilder.addHeader("testHeader","testValue");
                UrlRequest request = requestBuilder.build();

                // 发起请求
                request.start();
class MyUrlRequestCallback extends UrlRequest.Callback {
    private static final String TAG = "ttttt MyUrlRequestCallback";

    @Override
    public void onRedirectReceived(UrlRequest request, UrlResponseInfo info, String newLocationUrl) {
        android.util.Log.i(TAG, "onRedirectReceived method called.");
        // You should call the request.followRedirect() method to continue
        // processing the request.
        request.followRedirect();
    }


    @Override
    public void onResponseStarted(UrlRequest request, UrlResponseInfo info) {
        //这个函数只会调用一次
        android.util.Log.i(TAG, "onResponseStarted method called.");
        // You should call the request.read() method before the request can be
        // further processed. The following instruction provides a ByteBuffer object
        // with a capacity of 102400 bytes to the read() method.
        request.read(ByteBuffer.allocateDirect(102400));
    }

    @Override
    public void onFailed(UrlRequest urlRequest, UrlResponseInfo urlResponseInfo, CronetException cronetException) {

    }

    @Override
    public void onReadCompleted(UrlRequest request, UrlResponseInfo info, ByteBuffer byteBuffer) {
        //这个会调用多次
        android.util.Log.i(TAG, "onReadCompleted method called.");
        // You should keep reading the request until there's no more data.
        request.read(ByteBuffer.allocateDirect(102400));
    }

    @Override
    public void onSucceeded(UrlRequest request, UrlResponseInfo info, String str) {
        android.util.Log.i(TAG, "onSucceeded method called.");
    }
}


测试发现本地服务器正常收到请求了,测试app也能正常收到回调:



不过请求地址改成本地https的时候,发现崩溃,根据日志定位报错线程: 



发现是throw new UnsupportedOperationException("Method not decompiled造成的,那就是jadx代码不全的问题,这个参考GDA补全。

不过这里也看出是证书校验问题,本地的证书不通过。


// Certificate is not trusted due to non-trusted root of the certificate
// chain.
static int NO_TRUSTED_ROOT = -2;


这里又熟悉了下https,查了下资料:

HTTPS请求的整个过程的详细分析_研究生生活、学习记录-CSDN博客_https过程

最后,由于非对称加密的公钥可以在网络中传输,如何保证公钥传送到给正确的一方,这个时候使用了证书来验证。证书不是保证公钥的安全性,而是验证正确的交互方。



从这个流程图来看,访问本地网站就是验证证书无效了。


本地web服务器的file.crt文件:


解密后:

<Buffer 30 82 02 6d 30 82 01 d6 02 09 00 b9 de ff e9 ef 4c 9e 70 30 0d 06 09 2a 86 48 86 f7 0d 01 01 05 05 00 30 7a 31 0b 30 09 06 03 55 04 06 13 02 43 4e 31 ... 575 more bytes>

Len625


正好上面截图对应测试程序调试中的certChain。


搞清楚这个过程后,后面再进行干涉就有切入点了。



到这里后,虽然可以调用Cronet了,但是发现跟抖音自己的接口调用路径其实是不同的,比如某个接口的堆栈:

明显不是上面用的调用方式,那接着就是根据这个补代码了。

熟悉了上面这种注释语法




最后找到下面这个:



其实之前也看到过这个url,但是关联不到怎么调用的,现在看是通过代理类方式使用的($Proxy84),这里是这个功能的类定义文件,

单纯静态看确实不好看明白,通过动态调试才搞清楚整个过程。


看见有鸿蒙相关:


这里遇到个自己挖的坑,因为用了混淆的包名,导致hash这里跳过了,返回null了,会导致创建proxy类不成功:

private <T> T getStaticServiceImplReal(Class<T> cls) {
     PatchProxyResult proxy = PatchProxy.proxy(new Object[]{cls}, this, changeQuickRedirect, false, 95671);
     if (proxy.isSupported) {
         return (T) proxy.result;
     }
 
     int iHashCode=cls.getName().hashCode();
     switch (iHashCode) {


补完各种代码后(中间确实各种报错,各种补文件,最后备份的时候,发现有3000+文件,不过有的模块,比如wx相关的基本拷过来就能用了,zfb的交叉太多了,花了不少时间修复,感觉这样下去,可以编译一个抖音出来了),使用下面方式调用接口:

FeedActionApi.f71204b.diggItem("7018000130007633191", "7018000130007633191", 1, 0).get();


运行调试:

看起来点赞操作的类是创建成功了,跟hook看到的堆栈类似了:


这里查了下代理类的资料:


Java动态代理之InvocationHandler最简单的入门教程 - 简书 (jianshu.com)


创建代理类相关:









这个选择不同的网络模式,模拟器默认走第二种

        if (C9859c.m21219a()) {
            jSONObject.put("netClientType", "CronetClient");
        } else {
            jSONObject.put("netClientType", "TTOkhttp3Client");
        }


到这里后,终于在测试服务器收到请求了:




到这里的时候,发现极速版更新到18.2了

Cront相关Native函数引入有变化:



输出jni函数的时候,发现有SPDY相关的:



顺便查了下SPDY相关资料:

HTTP 的前世今生:一次性搞懂 HTTPHTTPSSPDYHTT_请求 (sohu.com)






后面看来要看下这个了。



在16.6版本都测试程序上替换上18.2版本都so,测试报错:


看起来native函数引入混淆了:

包装了一层:


对应修改后,就可以正常编译运行了。


看到检查root相关代码:


算起来这次搬代码,最难的是开始时候,加进来的代码牵扯其它引用,一堆编译错误,加入引用,可能又会引入新的引用依赖,很容易耐心磨没了,这个时候就需要权衡取舍,不能把分支展得太开,还好坚持下来了,慢慢框架搭起来后,很多就是正向工作了。


学习总结:

  1. 学习了JAV代理类使用,大厂设计模式。

  2. 熟悉了Cronet模块。

感觉主要工作都是正向的,正向逆向不分家,有了正向的工作,逆向切入点也会更多。


【看雪培训】目录重大更新!《安卓高级研修班》2022年春季班开始招生!

收藏
点赞3
打赏
分享
最新回复 (9)
雪    币: 902
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
jiyuren 活跃值 2021-12-2 09:47
2
0
干货满满,收藏一下
雪    币: 406
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_kputeuto 活跃值 2021-12-23 15:30
3
0
雪    币: 1454
活跃值: 活跃值 (4390)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
至尊小仙侠 活跃值 2021-12-23 16:03
4
0
牛逼 大佬
雪    币: 200
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
谈笑听风 活跃值 2021-12-30 14:41
5
0
请教下大佬,frida 怎么输出jni函数
雪    币: 1695
活跃值: 活跃值 (2291)
能力值: ( LV13,RANK:389 )
在线值:
发帖
回帖
粉丝
xwtwho 活跃值 1 2021-12-30 17:40
6
0
谈笑听风 请教下大佬,frida 怎么输出jni函数
https://github.com/lasting-yang/frida_hook_libart
雪    币: 200
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
谈笑听风 活跃值 2022-1-14 18:02
7
0
[RegisterNatives] method_count: 0x1
TypeError: cannot read property 'base' of null
    at onEnter (/hook_RegisterNatives.js:42)
[RegisterNatives] method_count: 0xc
[RegisterNatives] method_count: 0x9
TypeError: cannot read property 'base' of null
    at onEnter (/hook_RegisterNatives.js:42)
TypeError: cannot read property 'base' of null
    at onEnter (/hook_RegisterNatives.js:42)
打扰一下大佬,  报这个错怎么回事?
雪    币: 1695
活跃值: 活跃值 (2291)
能力值: ( LV13,RANK:389 )
在线值:
发帖
回帖
粉丝
xwtwho 活跃值 1 2022-1-17 12:37
8
0
没遇到过,我这个42行对不上你这个报错
脚本问题可以直接去问作者
也可以直接先拿自己的app测试,这样可以排除很多干扰
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
She_949461 活跃值 2022-3-3 17:22
9
0
有联系方式吗 私发 一下  
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
liukuo362573 活跃值 2022-3-22 16:05
10
0
大佬有联系方式吗 私发一个
游客
登录 | 注册 方可回帖
返回