安卓逆向 实战 某猫免费小说验证码请求协议分析&脱机执行

前言

这篇文章脱了好久才写完,眼看假期快结束了才快马加鞭的写完,有些地方可能写的也不是很清楚,等以后了再慢慢改进写作的技巧。该篇文章主要对一个小说软件的验证码请求协议进行了分析并实现了脱机模拟,详情见下文。

环境

pixel 6 android 12

frida 15.2.2

七猫免费小说 v7.2.3

frida检测

首先需要过掉frida的检测,这里可以参考这篇文章:手动编译Hluda Frida Server,使用去掉特征之后的frida-server可以过掉大部分检测,如果编译的过程遇到问题,也可以参考我对编译过程问题的记录:frida编译 hluda编译 问题记录,编译完之后直接以attach模式附加就可以过掉检测。

脱壳

通过MT管理器可以查看该应用是有壳的,如图:

在这里插入图片描述

我使用frida-dexdump进行脱壳,项目地址:frida-dexdump,运行效果如下:

在这里插入图片描述

sign算法分析

软件对代理有检测,使用justTrustMe可以过掉检测,项目地址:justTrustMe,过掉检测后使用HttpCanary进行抓包,抓到两个包,结果如下:

POST /api/v1/init/is-open-sm-code h2
Host: xiaoshuo.wtzw.com
net-env: 1
channel: qm-tengxun_lf
is-white: 0
platform: android
app-version: 70203
reg: 
application-id: com.kmxs.reader
authorization: 
qm-params: cLGUuq2-HTZ5gI9wgI9wgI9QNI0rp5U5pI4Mth9wgI9QgI9wgI9wgI9wgI9wH5w5pyRlmqN2tq2-HTZ5gT9LgT9EgTHnNhfEgIfn4THLgTKnNh4w4hKnpho2Aq0r4hNxpy0npqgMphOrNI9ngIoxgqkTpz0nNlFrphK5taGQ4qg5A5GIgTZekTZYNTZUNe1sgeZwN3HjHSNDuCGTpCR1paHWH-kRgSuwFq27mCo-3TkkO_FQqfrUmSoQtC2MFEsoufkRFljMgRGyRC2-gMGa4ROUOyNCf-QAk-pEp0gnq2kVRSoTResMpRx3kyNoRTo3k2p04C1kcygLmI05taGecCgQuzRLHTZ5ghH5taGMOSReuyR-tq2-HTZ5kofLuEssmqY1OqkiNoowuaUphTRVOqMQcCkIO0RUkoRImeFnf-pRcqFeF-GxReRw4Uu33MYykSu-FeomRy1qOqNCg_k2qoG04MRqgRGyR-kxc2or4eGZg3HjHz2Qpq-5A5H5taGQBlk2BaHWH2s1cyRjHI45taGEByHQmqU2m3HWH5HjHSuj45UUmqF5A5G0RhGEO0o1Bz2np0ZMfCsMtR2ANq1nB3UYu0NwkCR0RfNvNIo3k2RYpINaFzoCNCsTRUGth-pyulkIgR1fm2pn4UOwuyR4f-kTkR4nf-pqkyoWfCxTgzKnH5w54ln1pqYMtq2-HTZ5NhHENhFYAyFrNhG-pI-Lp5HjHzGL4qY-HTZ5plJDpln2H5w5Blo1paHWH5GJ
sign: c42882c83550414161a30aa63dbcdbce
qm-it: 1658476625
qm-ii: 1780595992
no-permiss: 3
user-agent: webviewversion/0
content-type: application/x-www-form-urlencoded
content-length: 84
accept-encoding: gzip

cancell_check=1&encrypt_phone=ghKrgeKUNh-rgI9=&sign=17f86fc9135531b6d3d8117a52914799
POST /api/v1/login/send-code h2
Host: xiaoshuo.wtzw.com
net-env: 1
channel: qm-tengxun_lf
is-white: 0
platform: android
app-version: 70203
reg: 
application-id: com.kmxs.reader
authorization: 
qm-params: cLGUuq2-HTZ5gI9wgI9wgI9QNI0rp5U5pI4Mth9wgI9QgI9wgI9wgI9wgI9wH5w5pyRlmqN2tq2-HTZ5gT9LgT9EgTHnNhfEgIfn4THLgTKnNh4w4hKnpho2Aq0r4hNxpy0npqgMphOrNI9ngIoxgqkTpz0nNlFrphK5taGQ4qg5A5GIgTZekTZYNTZUNe1sgeZwN3HjHSNDuCGTpCR1paHWH-kRgSuwFq27mCo-3TkkO_FQqfrUmSoQtC2MFEsoufkRFljMgRGyRC2-gMGa4ROUOyNCf-QAk-pEp0gnq2kVRSoTResMpRx3kyNoRTo3k2p04C1kcygLmI05taGecCgQuzRLHTZ5ghH5taGMOSReuyR-tq2-HTZ5kofLuEssmqY1OqkiNoowuaUphTRVOqMQcCkIO0RUkoRImeFnf-pRcqFeF-GxReRw4Uu33MYykSu-FeomRy1qOqNCg_k2qoG04MRqgRGyR-kxc2or4eGZg3HjHz2Qpq-5A5H5taGQBlk2BaHWH2s1cyRjHI45taGEByHQmqU2m3HWH5HjHSuj45UUmqF5A5G0RhGEO0o1Bz2np0ZMfCsMtR2ANq1nB3UYu0NwkCR0RfNvNIo3k2RYpINaFzoCNCsTRUGth-pyulkIgR1fm2pn4UOwuyR4f-kTkR4nf-pqkyoWfCxTgzKnH5w54ln1pqYMtq2-HTZ5NhHENhFYAyFrNhG-pI-Lp5HjHzGL4qY-HTZ5plJDpln2H5w5Blo1paHWH5GJ
sign: c42882c83550414161a30aa63dbcdbce
qm-it: 1658476625
qm-ii: 1780595992
no-permiss: 3
user-agent: webviewversion/0
content-type: application/x-www-form-urlencoded
content-length: 112
accept-encoding: gzip

encrypt_phone=ghKrgeKUNh-rgI9=&rid=20180817160958967672365a4t3bf655&type=0&sign=50b41c8edd846e92a4bdd0fdd6cabc6f

首先分析第一个包,根据url进行定位 /api/v1/init/is-open-sm-code

在这里插入图片描述

由注解这个特征可以判断使用了retrofit2框架,进入到@Body注明的类中查找,找到了"sign"

在这里插入图片描述

写frida脚本进行验证

function hook_2() {
    var is_sign = false
    let Buffer = Java.use("okio.Buffer");
    Buffer["writeUtf8"].overload('java.lang.String').implementation = function (str) {
        if (str == "sign") {
            is_sign = true
        }
        let ret = this.writeUtf8(str);
        return ret;
    };

    let Encryption = Java.use("com.qimao.qmsdk.tools.encryption.Encryption");
    Encryption["sign"].implementation = function (str) {
        let ret = this.sign(str);
        if (is_sign) {
            console.log("Encryption.sign arg-->" + str)
            console.log("sign=" + ret)
            is_sign = false
        }
        return ret;
    };
}

function main() {
    Java.perform(function () {
        hook_2()
    })
}
setImmediate(main)

脚本运行结果如下:

在这里插入图片描述

经过对比,Encryption.sign的返回值就是两个包的body里面sign的值,下面分析Encryption.sign

在这里插入图片描述

是native方法,首先我们需要定位so文件,我先判断是不是静态注册,使用了frida-trace,这里要注意静态方法的命名规则(以字符串“Java”为前缀,并且用“_”下划线将包名、类名以及native方法名连接起来)

在这里插入图片描述

看来是静态注册,并定位到了libcommon-encryption.so,用IDA打开之后搜索,之后发现了一个很诡异的问题,在方法里面搜不到Java_com_km_encryption_api_Security_sign,如下图:

在这里插入图片描述

虽然没有这个方法名,但我们也可以通过偏移进行定位,frida脚本如下:

var module = Process.findModuleByName("libcommon-encryption.so")
var funcs = module.enumerateExports()
funcs.forEach(function(func){
    if(func.name.indexOf("Java_com_km_encryption_api_Security_sign")>=0){
        console.log("sign offset-->0x" + (func.address-module.base).toString(16))
    }
})

在这里插入图片描述

拿到了偏移0x15620,跳转过去看一下

在这里插入图片描述

我不知道为什么,这里显示的是token,难道两个函数能注册到同一个地址?但不论发生了什么,我们可以验证这是不是sign的逻辑

在这里插入图片描述

这个函数最终返回了jstring,因此构造frida代码如下:

var env = Java.vm.tryGetEnv()
Interceptor.attach(module.base.add(0x15620), {
    onLeave:function(retval){
        var env = Java.vm.tryGetEnv()
        console.log(env.getStringUtfChars(retval,0).readCString())
    }
})

同java层的sign函数和token函数一起hook,结果如下:

在这里插入图片描述

发现虽然这个函数名是Java_com_km_encryption_api_Security_token,但是却也是Java_com_km_encryption_api_Security_sign的逻辑,下面直接分析逻辑

在这里插入图片描述

这个函数的逻辑非常简单,就是先生成KeyData作为MD5的盐,然后把它拼接到字符串的后面,最后进行md5运算,用frida脚本进行验证,如下:

function hook_2() {
    var is_sign = false
    let Buffer = Java.use("okio.Buffer");
    Buffer["writeUtf8"].overload('java.lang.String').implementation = function (str) {
        if (str == "sign") {
            is_sign = true
        }
        let ret = this.writeUtf8(str);
        return ret;
    };

    let Encryption = Java.use("com.qimao.qmsdk.tools.encryption.Encryption");
    Encryption["sign"].implementation = function (str) {
        let ret = this.sign(str);
        if (is_sign) {
            console.log("Encryption.sign arg-->" + str)
            console.log("sign=" + ret)
            is_sign = false
        }
        return ret;
    };
}

function hook_so() {
    var libcommon = Process.findModuleByName("libcommon-encryption.so")
	Interceptor.attach(libcommon.base.add(0x1574C), {
        onEnter: function (args) {
            console.log("---keydata---");
            console.log(hexdump(this.context.x1, {
                offset: 0,
                length: 128,
                header: true,
                ansi: true
            }));

            console.log("---keydataSize---");
            console.log("size-->" + this.context.x2);
        }
    })

    var data = null
    Interceptor.attach(libcommon.base.add(0x15730), {
        onEnter: function (args) {
            data = this.context.x0
        }
    })
    Interceptor.attach(libcommon.base.add(0x1575C), {
        onEnter: function (args) {
            if (data != null) {
                console.log("---data---");
                console.log(hexdump(data, {
                    offset: 0,
                    length: 128,
                    header: true,
                    ansi: true
                }));
            }
        }
    })
}
function main() {
    Java.perform(function () {
        hook_2()
    })
    hook_so()
}
setImmediate(main)

运行结果如下:

在这里插入图片描述

在上图中—keydata—是md5的盐,其值为d3dGiJc651gSQ8w1,—data—下的内容就是字符串拼接后的结果,sign就是该函数的返回值,用CyberChef验证一下

encrypt_phone=ghfYAIOMNhKYNTf=rid=20180817160958967672365a4t3bf655type=0d3dGiJc651gSQ8w1

在这里插入图片描述

与函数返回值一致,sign算法分析结束

encrypt_phone加密分析

现在让我们缕一缕分析的情况,如今sign加密函数已经分析出来了,sign这个字段在请求头与请求体中都有出现,我们不知道的是这两个sign是否是经过同一个加密函数加密的,也不知道sign函数传入的明文是如何生成的。hook sign代码如下:

var security = Java.use("com.km.encryption.api.Security")
security.sign.implementation = function(){
	console.log("++++++++++++++++++++++");
	var str = Java.use("java.lang.String").$new(arguments[0])
	console.log("arguments -->",str);
	var ret = this.sign.apply(this,arguments)
	console.log("result -->",ret)
	console.log("++++++++++++++++++++++");
	return ret
}

运行结果分析:

请求头

在这里插入图片描述

在这里插入图片描述

请求体(包1):

在这里插入图片描述

在这里插入图片描述

请求体(包2):

在这里插入图片描述

在这里插入图片描述

经过以上的整理我们可以得到两点:1. 请求头与请求体中的sign都由同一个加密函数加密 2. 获得了传入加密函数的字符串

经过多次抓包发现请求头里的sign以及qm-params是不会发生变化的,因此跳过对其的分析,直接开始分析请求体中的sign加密时传入的参数

其中只有encrypt_phone发生了变化,看着像base64,尝试用CyberChef进行base64解码

在这里插入图片描述

并没有解码成电话号码,那只能逆向分析了,在刚开始我们就已经定位到了请求体中sign的拼接位置,encrypt_phone是挨着的,代码如下:

在这里插入图片描述

为了确保encrypt_phone的位置没有找错,需要看一下this.f14735a的值,脚本及运行结果如下:

var is_sign = false
let Buffer = Java.use("okio.Buffer");
Buffer["writeUtf8"].overload('java.lang.String').implementation = function (str) {
    if (str == "sign") {
        is_sign = true
        Java.choose("yh0", {
            onMatch: function (instance) {
                console.log(instance._a.value);
            },
            onComplete: function () {

            }
        })
    }
    let ret = this.writeUtf8(str);
    return ret;
};

let Encryption = Java.use("com.qimao.qmsdk.tools.encryption.Encryption");
Encryption["sign"].implementation = function (str) {
    let ret = this.sign(str);
    if (is_sign) {
        console.log("Encryption.sign arg-->" + str)
        console.log("sign=" + ret)
        is_sign = false
    }
    return ret;
};

在这里插入图片描述

结果中有encrypt_phone,说明我们找对位置了,那么我们就要寻找this.f14735a的赋值位置

在这里插入图片描述

在这里插入图片描述

红框中即为this.f14735a的赋值位置,也是encrypt_phone的赋值位置,上面代码的逻辑是把传入a方法的t转换成json,然后取出每一个元素进行赋值,那么我们需要知道t是什么,脚本如下:

let can_hook = false
let yh0 = Java.use("yh0");
yh0["a"].implementation = function (t) {
	can_hook = true
	let ret = this.a(t);
	return ret;
};

let NBSGsonInstrumentation = Java.use("com.networkbench.agent.impl.instrumentation.NBSGsonInstrumentation");
NBSGsonInstrumentation["toJson"].overload('com.google.gson.Gson', 'java.lang.Object').implementation = function (gson, obj) {
	let ret = this.toJson(gson, obj);
	if (can_hook) {
		console.log("t value is " + obj);
		console.log('toJson ret value is ' + ret);
	}
	can_hook = false
	return ret;
};

在这里插入图片描述

t是一个类的对象,我们先看看com.qimao.qmuser.model.entity.CaptchaEntity这个类

在这里插入图片描述

里面有encrypt_phone,继续寻找其赋值位置

在这里插入图片描述

直接打印调用栈,脚本如下:

在这里插入图片描述

经过一系列的调用定位到ag0.onNext,代码如下:

在这里插入图片描述

这很明显是一个回调方法,大概率的使用了框架,来看一下该类继承的类

在这里插入图片描述

谷歌了一下,这个类使用了RxJava框架,参考文章:http://www.jianshu.com/p/a406b94f3188,该框架的核心代码如下:

Observable.create(new ObservableOnSubscribe<Integer>() {
        // 1. 创建被观察者 & 生产事件
            @Override
            public void subscribe(ObservableEmitter<Integer> emitter) throws Exception {
                emitter.onNext(1);
                emitter.onNext(2);
                emitter.onNext(3);
                emitter.onComplete();
            }
        }).subscribe(new Observer<Integer>() {
            // 2. 通过通过订阅(subscribe)连接观察者和被观察者
            // 3. 创建观察者 & 定义响应事件的行为
            @Override
            public void onSubscribe(Disposable d) {
                Log.d(TAG, "开始采用subscribe连接");
            }
            // 默认最先调用复写的 onSubscribe()

            @Override
            public void onNext(Integer value) {
                Log.d(TAG, "对Next事件"+ value +"作出响应"  );
            }

            @Override
            public void onError(Throwable e) {
                Log.d(TAG, "对Error事件作出响应");
            }

            @Override
            public void onComplete() {
                Log.d(TAG, "对Complete事件作出响应");
            }

        });

为了能够定位到关键代码,我选择hook Observable.subscribe(Observer)这个方法,脚本如下:

let Observable = Java.use("io.reactivex.Observable")
Observable.subscribe.overload('io.reactivex.Observer').implementation = function(){
	console.log("+++++++++++++++++");
	console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
	let ret = this.subscribe.apply(this,arguments)
	return ret
}

在这里插入图片描述

vo0.a的代码如下:

在这里插入图片描述

关键是new b(strArr),具体原因可以学习一下RxJava,点击去看一下

在这里插入图片描述

该类继承自Callable,当Callable创建时自动调用call()方法,而call()方法里面的逻辑就是取出this.fq4329a中的值进行加密,进入到encrypt(str)方法中看一下

在这里插入图片描述

很简单,就是先进行base64编码,然后对编码后的结果进行字符替换,replaceChar©的代码如下:

在这里插入图片描述

脱机

那么到这里所有需要分析的字段已经分析完了,接下来使用python进行模拟调用,具体代码不贴了,只展示最后的结果,如图:

在这里插入图片描述

哇,您们居然能看到这里,真棒呀 (´∀`)

  • 4
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 《Android应用安全实战:Frida协议分析》是一本介绍Frida工具的使用和Android应用安全分析的书籍。Frida是一个强大的动态分析工具,可以帮助开发人员和安全研究人员对应用进行动态分析。本书以Frida为基础,介绍了如何对一些常见的安全问题进行定位和修复。 本书主要分为三部分,第一部分介绍了Frida工具的安装和使用方法,包括如何在Windows、Mac和Ubuntu三个系统上安装Frida,以及如何使用Frida对应用进行动态分析和漏洞挖掘。第二部分介绍了一些常见的安全问题,如反调试、class tampering、hook、rpc等问题,并详细介绍了如何通过Frida对这些问题进行分析和修复。第三部分介绍了一些应用案例分析,以及如何通过Frida对应用中的加密算法、网络协议、第三方代码等进行分析。 本书的特点在于实战性强,作者通过大量的实例和案例,让读者能够更加深入地理解Frida工具的使用方法,并能够将所学知识应用到实际的项目中。同时,本书还提供了一些工具和脚本,方便读者能够更加快速地进行分析和修复工作。 总的来说,本书是一本对于安卓开发人员、移动安全研究人员和安全工程师来说非常有价值的工具书,无论是对于入门和提高都有很大的帮助。Frida工具的特点在于动态分析,能够帮助开发人员和安全工程师快速定位和修复常见的安全问题。而本书则是一个详细的实例教程,通过这份教程的学习,读者将能够掌握Frida工具的使用方法,并能够熟练地应用到实际的项目中。 ### 回答2: 《Android应用安全实战:Frida协议分析PDF》是一本针对移动应用安全实战指南。这本书主要讲述了如何借助Frida这个强大的工具来进行移动应用的安全分析和漏洞挖掘。 Frida是一款可用于对移动应用进行实时动态分析的工具,它的优点在于可以轻松地hook任意函数和类,还可以在不用重新编译应用程序的情况下动态修改应用程序的行为。Frida的这些特性使得它成为了许多黑客和渗透测试人员使用的首选工具之一。 在《Android应用安全实战:Frida协议分析PDF》中,作者首先介绍了Frida工具的基本原理和使用方法,包括安装和配置Frida、使用Frida脚本、使用Frida进行hook等。接着,作者详细介绍了Android应用程序的各个组成部分和重要的安全机制,并给出了许多实例来演示如何通过Frida来绕过这些安全机制。 最后,作者还介绍了一些常用的Frida脚本和工具,比如Hooking SSLPinning脚本、Frida-Extract工具、在Android 8.0上使用Frida等。这些工具和脚本可以帮助读者更快更高效地进行移动应用的安全分析和漏洞挖掘。 总的来说,《Android应用安全实战:Frida协议分析PDF》是一本非常实用的书籍,对于渗透测试人员、黑客和移动应用开发人员来说都具有一定的参考价值。通过学习和掌握Frida这个工具,可以让我们更好地发现和修复移动应用程序中的安全问题,从而提升我们的移动应用程序的安全性和可靠性。 ### 回答3: 《Android应用安全实战:Frida协议分析》是一本关于安卓应用安全实战教程。该书的重点在于介绍如何使用Frida协议进行应用程序的逆向分析,得出应用程序的安全漏洞,并给出相应的解决方案。 Frida是一个能够在运行时动态注入JavaScript并调试代码的工具。这也意味着Frida能够直接访问运行在内存中的应用程序。这个优势使Frida成为了一个强大的安卓应用程序分析和调试工具。通过使用Frida,用户可以对应用程序进行监视、修改、甚至是攻击。 《Android应用安全实战:Frida协议分析》详细介绍了Frida的使用方法,包括Frida的架构、基本操作、配置和调试应用程序等。此外,该书还介绍了有关安卓应用程序逆向工程和安全审计的实操技能。通过本书的学习,在安卓应用程序的开发过程中,读者可以掌握一定的安全知识和技能,从而提高应用程序的安全性,避免攻击和漏洞的发生。 总之,《Android应用安全实战:Frida协议分析》是一本非常实用、易懂的安卓应用程序安全入门教材。在阅读该书并掌握其中的技能后,用户可以应用这些技能进行应用程序的逆向分析安全审计,从而达到提高安卓应用程序安全性的目的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值