Android reverse actual combat against a cat free novel verification code request protocol analysis & offline execution

foreword

It took me a long time to finish writing this article, and I finished it as soon as the holiday was almost over. Some parts may not be very clear, and I will slowly improve my writing skills in the future. This article mainly analyzes the verification code request protocol of a novel software and realizes offline simulation, see below for details.

environment

pixel 6 android 12

frida 15.2.2

Seven cats free novel v7.2.3

frida detection

First of all, you need to pass the detection of frida. Here you can refer to this article: Manually compile Hluda Frida Server , and use the frida-server after removing the features to pass most of the detection. If you encounter problems during the compilation process, you can also refer to my compilation Records of process problems: frida compilation hluda compilation problem records , after compilation, directly attach in attach mode to pass the detection.

shelling

Through the MT manager, you can see that the application has a shell, as shown in the figure:

insert image description here

I use frida-dexdump to unpack, the project address: frida-dexdump , the operation effect is as follows:

insert image description here

Sign algorithm analysis

The software detects the agent, using justTrustMe can pass the detection, the project address: justTrustMe , after passing the detection, use HttpCanary to capture packets, capture two packets, the results are as follows:

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

First analyze the first package and locate /api/v1/init/is-open-sm-code according to the url

insert image description here

From the feature of the annotation, it can be judged that the retrofit2 framework is used, enter the class marked by @Body to search, and find "sign"

insert image description here

Write frida script for verification

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)

The result of running the script is as follows:

insert image description here

After comparison, the return value of Encryption.sign is the value of sign in the body of the two packages. Let's analyze Encryption.sign

insert image description here

It is a native method. First of all, we need to locate the so file. I first judge whether it is statically registered or not. I use frida-trace. Here, we should pay attention to the naming rules of static methods (prefixed with the string "Java" and underlined with "_") package name, class name and native method name)

insert image description here

It seems to be a static registration, and located libcommon-encryption.so, opened it with IDA and searched, and then found a very strange problem, Java_com_km_encryption_api_Security_sign could not be found in the method, as shown below:

insert image description here

Although there is no such method name, we can also locate by offset. The frida script is as follows:

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))
    }
})

insert image description here

Got the offset 0x15620, jump over to have a look

insert image description here

I don't know why, the token is shown here, can two functions be registered to the same address? But no matter what happens, we can verify that this is the logic of sign

insert image description here

This function finally returns jstring, so the code to construct frida is as follows:

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())
    }
})

Hook together with the sign function and token function of the java layer, the result is as follows:

insert image description here

It is found that although the function name is Java_com_km_encryption_api_Security_token, it is also the logic of Java_com_km_encryption_api_Security_sign. The following directly analyzes the logic

insert image description here

The logic of this function is very simple. It is to first generate KeyData as the salt of MD5, then splice it to the back of the string, and finally perform md5 calculation, and use the frida script to verify it, as follows:

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)

The result of the operation is as follows:

insert image description here

In the figure above——keydata—is the salt of md5, its value is d3dGiJc651gSQ8w1, the content under—data—is the result of string splicing, and sign is the return value of this function. Verify it with CyberChef

encrypt_phone=ghfYAIOMNhKYNTf=rid=20180817160958967672365a4t3bf655type=0d3dGiJc651gSQ8w1

insert image description here

Consistent with the return value of the function, the sign algorithm analysis ends

encrypt_phone encryption analysis

Now let's analyze the situation one by one. Now that the sign encryption function has been analyzed, the sign field appears in both the request header and the request body. What we don't know is whether the two signs are encrypted by the same encryption function. I don't know how the plaintext passed in by the sign function is generated. The hook sign code is as follows:

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
}

Run result analysis:

Request header :

insert image description here

insert image description here

Request body (package 1):

insert image description here

insert image description here

Request body (package 2):

insert image description here

insert image description here

After the above sorting, we can get two points: 1. The sign in the request header and the request body are both encrypted by the same encryption function 2. The string passed to the encryption function is obtained

After many packet captures, it is found that the sign and qm-params in the request header will not change, so the analysis is skipped, and the parameters passed in when the sign in the request body is encrypted are directly analyzed

Among them, only encrypt_phone has changed, it looks like base64, try to use CyberChef to decode base64

insert image description here

It is not decoded into a phone number, so it can only be analyzed in reverse. At the beginning, we have located the splicing position of the sign in the request body. The encrypt_phone is next to each other. The code is as follows:

insert image description here

In order to ensure that the location of encrypt_phone is not wrong, you need to look at the value of this.f14735a. The script and running results are as follows:

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

insert image description here

There is encrypt_phone in the result, indicating that we have found the right location, then we need to find the assignment location of this.f14735a

insert image description here

insert image description here

The red box is the assignment location of this.f14735a, and also the assignment location of encrypt_phone. The logic of the above code is to convert the t passed into the a method into json, and then take out each element for assignment. Then we need to know what t is. The script is as follows:

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

insert image description here

t is an object of a class, let's first look at the class com.qimao.qmuser.model.entity.CaptchaEntity

insert image description here

There is encrypt_phone inside, continue to look for its assignment location

insert image description here

Print the call stack directly, the script is as follows:

insert image description here

After a series of calls to locate ag0.onNext, the code is as follows:

insert image description here

This is obviously a callback method, and the framework is used with a high probability. Let's take a look at the classes inherited by this class

insert image description here

Google it, this class uses the RxJava framework, refer to the article: http://www.jianshu.com/p/a406b94f3188, the core code of the framework is as follows:

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事件作出响应");
            }

        });

In order to be able to locate the key code, I choose the hook Observable.subscribe(Observer) method, the script is as follows:

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
}

insert image description here

The code of vo0.a is as follows:

insert image description here

The key is new b(strArr). For the specific reason, you can learn about RxJava. Click to see it

insert image description here

This class inherits from Callable. When Callable is created, the call() method is automatically called, and the logic in the call() method is to take out the value in this.fq4329a for encryption, and enter the encrypt(str) method to have a look

insert image description here

It is very simple, that is, base64 encoding is performed first, and then character replacement is performed on the encoded result. The code of replaceChar© is as follows:

insert image description here

offline

So here all the fields that need to be analyzed have been analyzed, and then use python to simulate the call. The specific code will not be posted, and only the final result will be displayed, as shown in the figure:

insert image description here

Wow, it's awesome that you can see this (´∀`)

Guess you like

Origin blog.csdn.net/weixin_56039202/article/details/126376536