Analysis of the login protocol of a sports APP

foreword

Recently, I am trying to explore in the reverse direction. I have analyzed the login protocol of a certain sports APP. This article records the analysis process and results. It is only for learning and research. There are few tools used and the content is relatively simple. Novice items, large Dude please skip. For the analysis of the password login module, just input a mobile phone number and password, and then use the packet capture tool to analyze it. It is worth looking at the implementation logic for several fields of the login protocol from a learning perspective.

grab bag

  1. Capture packets using Charles, please install and configure the certificate yourself
  2. Grab the login interface and click the password to log in. Use fake account secrets to test the packet capture, and the packet capture can be successful

    image description

Sign analysis

First, you can see that there is a sign field in the request header, and analyze this field:

sign: b61df9a8bce7a8641c5ca986b55670e633a7ab29

The overall length is 40, and the commonly used MD5 length is 32. The first reaction is not very similar, but it is also possible to splice other fields after md5. The length of the sha1 hash function is 40, which is exactly the same. Then we will verify one by one, first check whether there is any trace of MD5, and directly write the script frida and try to run it. The content of the script is relatively clear. For the Init, Update, and Final of MD5, hook and print to see the input and output. The key code is given below:

// hook CC_MD5

// unsigned char * CC_MD5(const void *data, CC_LONG len, unsigned char *md);

Interceptor.attach(Module.findExportByName("libcommonCrypto.dylib", g_funcName), {

    onEnter: function(args) {

        console.log(g_funcName + " begin");

        var len = args[1].toInt32();

        console.log("input:");

        dumpBytes(args[0], len);

        this.md = args[2];

    },

    onLeave: function(retval) {

        console.log(g_funcName + " return value");

        dumpBytes(this.md, g_funcRetvalLength);

        console.log(g_funcName + ' called from:\n' +

            Thread.backtrace(this.context, Backtracer.ACCURATE)

            .map(DebugSymbol.fromAddress).join('\n') + '\n');

    }

});

// hook CC_MD5_Update

// int CC_MD5_Update(CC_MD5_CTX *c, const void *data, CC_LONG len);

Interceptor.attach(Module.findExportByName("libcommonCrypto.dylib", g_updateFuncName), {

    onEnter: function(args) {

        console.log(g_updateFuncName + " begin");

        var len = args[2].toInt32();

        console.log("input:");

        dumpBytes(args[1], len);

    },

    onLeave: function(retval) {

        console.log(g_updateFuncName + ' called from:\n' +

            Thread.backtrace(this.context, Backtracer.ACCURATE)

            .map(DebugSymbol.fromAddress).join('\n') + '\n');

    }

});

// hook CC_MD5_Final

// int CC_MD5_Final(unsigned char *md, CC_MD5_CTX *c);

Interceptor.attach(Module.findExportByName("libcommonCrypto.dylib", g_finalFuncName), {

    onEnter: function(args) {

        //console.log(func.name + " begin");

        finalArgs_md = args[0];

    },

    onLeave: function(retval) {

        console.log(g_finalFuncName + " return value");

        dumpBytes(finalArgs_md, g_funcRetvalLength);

        console.log(g_finalFuncName + ' called from:\n' +

            Thread.backtrace(this.context, Backtracer.ACCURATE)

            .map(DebugSymbol.fromAddress).join('\n') + '\n');

    }

});

 

Fortunately, the content related to the sign can be clearly seen in the printing, but the latter part of the sign is missing, so it is clear that the composition of the sign value is 32(md5)+8, first look at the data construction process of md5.

b61df9a8bce7a8641c5ca986b55670e6  33a7ab29

image description

 

 

It can be clearly seen by printing that the MD5 of sign consists of three parts of data, namely: bodyData+Url+Str, and the body data can also be obtained from Charles.

  • {"body":"5gJEXtLqe3tzRsP8a/bSwehe0ta3zQx6wG7K74sOeXQ6Auz1NI1bg68wNLmj1e5Xl7CIwWelukC445W7HXxJY6nQ0v0SUg1tVyWS5L8E2oaCgoSeC6ypFNXV2xVm8hHV"}

  • /account/v4/login/password

  • V1QiLCJhbGciOiJIUzI1NiJ9

  • image description

     

    image description

     

    Sign tail analysis

    Next, we analyze the tail data of Sign. Simply blind guessing or hanging frida scripts can’t solve the problem. Let’s use IDA to see the specific implementation logic. Of course, the above MD5 analysis can also be directly decompiled from IDA. Search The sign keyword is used for positioning, but I am used to hooking the script first, and if it hits directly, I don’t need to spend time analyzing it...

    Through the MD5 script printing, we can also see the related function call stack, which also provides great convenience for us to quickly locate. We directly search for the [KEPPostSecuritySign kep_signWithURL: body:] method, and we can see obvious traces of string splicing. IDA is relatively intelligent and has recognized the salt value of MD5.

  •  

    image description

     

Through the analysis, locate the [NSString kep_networkStringOffsetSecurity] function, process the string internally, and perform various judgments and shift operations in the loop. If you don’t mind the trouble, you can analyze the logic and rewrite the processing flow. 

 

image description

 

Hook for encryption algorithms such as AES128, DES, 3DES, CAST, RC4, RC2, Blowfish, etc. The key code of the script is as follows: 

var handlers = {

    CCCrypt: {

        onEnter: function(args) {

            var operation = CCOperation[args[0].toInt32()];

            var alg = CCAlgorithm[args[1].toInt32()].name;

            this.options = CCoptions[args[2].toInt32()];

            var keyBytes = args[3];

            var keyLength = args[4].toInt32();

            var ivBuffer = args[5];

            var inBuffer = args[6];

            this.inLength = args[7].toInt32();

            this.outBuffer = args[8];

            var outLength = args[9].toInt32();

            this.outCountPtr = args[10];

            if (this.inLength < MIN_LENGTH || this.inLength > MAX_LENGTH){

                return;

            }

            if (operation === "kCCEncrypt") {

                this.operation = "encrypt"

                console.log("***************** encrypt begin **********************");

            else {

                this.operation = "decrypt"

                console.log("***************** decrypt begin **********************");

            }

            console.log("CCCrypt(" +

                "operation: " this.operation + ", " +

                "CCAlgorithm: " + alg + ", " +

                "CCOptions: " this.options + ", " +

                "keyBytes: " + keyBytes + ", " +

                "keyLength: " + keyLength + ", " +

                "ivBuffer: " + ivBuffer + ", " +

                "inBuffer: " + inBuffer + ", " +

                "inLength: " this.inLength + ", " +

                "outBuffer: " this.outBuffer + ", " +

                "outLength: " + outLength + ", " +

                "outCountPtr: " this.outCountPtr + ")"

            );

            //console.log("Key: utf-8 string:" + ptr(keyBytes).readUtf8String())

            //console.log("Key: utf-16 string:" + ptr(keyBytes).readUtf16String())

            console.log("key: ");

            dumpBytes(keyBytes, keyLength);

            console.log("IV: ");

            // ECB模式不需要iv,所以iv是null

            dumpBytes(ivBuffer, keyLength);

            var isOutput = true;

            if (!SHOW_PLAIN_AND_CIPHER && this.operation == "decrypt") {

                isOutput = false;

            }

            if (isOutput){

                // Show the buffers here if this an encryption operation

                console.log("In buffer:");

                dumpBytes(inBuffer, this.inLength);

            }

             

        },

        onLeave: function(retVal) {

            // 长度过长和长度太短的都不要输出

            if (this.inLength < MIN_LENGTH || this.inLength > MAX_LENGTH){

                return;

            }

            var isOutput = true;

            if (!SHOW_PLAIN_AND_CIPHER && this.operation == "encrypt") {

                isOutput = false;

            }

            if (isOutput) {

                // Show the buffers here if this a decryption operation

                console.log("Out buffer:");

                dumpBytes(this.outBuffer, Memory.readUInt(this.outCountPtr));

            }

            // 输出调用堆栈,会识别类名函数名,非常好用

            console.log('CCCrypt called from:\n' +

                Thread.backtrace(this.context, Backtracer.ACCURATE)

                .map(DebugSymbol.fromAddress).join('\n') + '\n');

        }

    },

};

if (ObjC.available) {

    console.log("frida attach");

    for (var func in handlers) {

        console.log("hook " + func);

        Interceptor.attach(Module.findExportByName("libcommonCrypto.dylib", func), handlers[func]);

    }

else {

    console.log("Objective-C Runtime is not available!");

}

 

Looking at the output log of the script, it directly hits the encryption algorithm of AES128, and the output Base64 data matches exactly. It can only be said that luck is overwhelming. 

 

 

After a simple transformation, even if the script can hook to the corresponding function, it is still not possible to directly call the result offline. At this time, it is necessary to perform decompilation analysis or dynamic debugging. At this time, it is necessary to cooperate with static protection methods such as code obfuscation and VMP , coupled with security measures such as anti-debugging, the threshold for attacks is also increased accordingly. 

Guess you like

Origin blog.csdn.net/q2919761440/article/details/132216394