协议枚举、爆破及算法模拟

xiaoeryu Lv5

很多的加密算法是在so中编写的,并且加了混淆,死磕的话逆向难度是比较大的,在不必要的情况下可以尝试去主动调用算法实现目的:

测试环境:

测试用的demo

这里使用我们前面写的脚本来抓包,因为协议都是TCP所以可以使用我们前面写的抓取javaTCP的代码,再加上libc

上代码

function LogPrint(log) {
    var theDate = new Date();
    var hour = theDate.getHours();
    var minute = theDate.getMinutes();
    var second = theDate.getSeconds();
    var mSecond = theDate.getMilliseconds();

    hour < 10 ? hour = "0" + hour : hour;
    minute < 10 ? minute = "0" + minute : minute;
    second < 10 ? second = "0" + second : second;
    mSecond < 10 ? mSecond = "00" + mSecond : mSecond < 100 ? mSecond = "0" + mSecond : mSecond;
    var time = hour + ":" + minute + ":" + second + ":" + mSecond;
    var threadid = Process.getCurrentThreadId();
    console.log("[" + time + "]" + "->threadid:" + threadid + "--" + log);

}

function printJavaStack(name) {
    Java.perform(function () {
        var Exception = Java.use("java.lang.Exception");
        var ins = Exception.$new("Exception");
        var straces = ins.getStackTrace();
        if (straces != undefined && straces != null) {
            var strace = straces.toString();
            var replaceStr = strace.replace(/,/g, " \n ");
            LogPrint("=============================" + name + " Java Stack strat=======================");
            LogPrint(replaceStr);
            LogPrint("=============================" + name + " Java Stack end======================= \n ");
            Exception.$dispose();
        }
    });
}
function printNativeStack(context, name) {
    var array = Thread.backtrace(context, Backtracer.ACCURATE);
    var trace = array.map(DebugSymbol.fromAddress).join("\n");
    LogPrint("=============================" + name + " Native Stack strat=======================");
    LogPrint(trace);
    LogPrint("=============================" + name + " Native Stack end======================= \n ");
}

function isprintable(value) {
    if (value >= 32 && value <= 126) {
        return true;
    }
    return false;
}

function getsocketdetail(fd) {
    var type = Socket.type(fd);
    if (type !== null) {
        var peer = Socket.peerAddress(fd);
        var local = Socket.localAddress(fd);
        // return `type:${type}, address:${JSON.stringify(peer)}, local:${JSON.stringify(local)}`;
        return ("type: " + type + ", address: " + JSON.stringify(peer) + ", local: " + JSON.stringify(local));
    }
    return "unknown";
}

function hooktcp() {
    Java.perform(function () {
        var SocketClass = Java.use('java.net.Socket');
        SocketClass.$init.overload('java.lang.String', 'int').implementation = function (arg0, arg1) {
            console.log("[" + Process.getCurrentThreadId() + "]new Socket connection: " + arg0 + ",port: " + arg1);
            printJavaStack('tcp connect...')
            return this.$init(arg0, arg1);
        }
        var SocketInputStreamClass = Java.use('java.net.SocketInputStream');
        //socketRead0
        SocketInputStreamClass.socketRead0.implementation = function (arg0, arg1, arg2, arg3, arg4) {
            var size = this.socketRead0(arg0, arg1, arg2, arg3, arg4);
            //console.log("[" + Process.getCurrentThreadId() + "]socketRead0:size:" + size + ",content:" + JSON.stringify(arg1));
            var bytearray = Java.array('byte', arg1);
            var content = '';
            for (var i = 0; i < size; i++) {
                if (isprintable(bytearray[i])) {
                    content = content + String.fromCharCode(bytearray[i]);
                }
            }
            var socketimpl = this.impl.value;
            var address = socketimpl.address.value;
            var port = socketimpl.port.value;

            console.log("\naddress:" + address + ",port: " + port + "\n" + JSON.stringify(this.socket.value) + "\n[" + Process.getCurrentThreadId() + "]receive: " + content);
            printJavaStack('socketRead0')
            return size;
        }
        var SocketOutPutStreamClass = Java.use('java.net.SocketOutputStream');
        SocketOutPutStreamClass.socketWrite0.implementation = function (arg0, arg1, arg2, arg3) {
            var result = this.socketWrite0(arg0, arg1, arg2, arg3);
            //console.log("[" + Process.getCurrentThreadId() + "]socketWrite0:len:" + arg3 + "--content:" + JSON.stringify(arg1));
            var bytearray = Java.array('byte', arg1);
            var content = '';
            for (var i = 0; i < arg3; i++) {

                if (isprintable(bytearray[i])) {
                    content = content + String.fromCharCode(bytearray[i]);
                }
            }
            var socketimpl = this.impl.value;
            var address = socketimpl.address.value;
            var port = socketimpl.port.value;
            console.log("send address:" + address + ",port: " + port + "[" + Process.getCurrentThreadId() + "]send: " + content);
            console.log("\n" + JSON.stringify(this.socket.value) + "\n[" + Process.getCurrentThreadId() + "]send: " + content);
            printJavaStack('socketWrite0')
            return result;
        }
    })
}

function hooklibc() {
    var libcmodule = Process.getModuleByName("libc.so");
    var recvfrom_addr = libcmodule.getExportByName("recvfrom");
    var sendto_addr = libcmodule.getExportByName("sendto");
    // console.log(`${recvfrom_addr} --- ${sendto_addr}`);
    console.log(recvfrom_addr + "---" + sendto_addr);

    Interceptor.attach(recvfrom_addr, {
        onEnter: function (args) {
            this.arg0 = args[0];
            this.arg1 = args[1];
            this.arg2 = args[2];
            this.arg3 = args[3];
            this.arg4 = args[4];
            this.arg5 = args[5];

            LogPrint("go into libc.so->recvfrom");

            var result = getsocketdetail(this.arg0.toInt32());
            if (result.indexOf("udp") > 0) {
                handleUdp('recvfrom', this.arg4, this.arg5);
            }

            printNativeStack(this.context, "recvfrom");
        },
        onLeave: function (retval) {
            var size = retval.toInt32();
            if (size > 0) {
                var result = getsocketdetail(this.arg0.toInt32());
                // console.log(`${result} --- libc.so->recvfrom: ${hexdump(this.arg1, { length: size })}`);
                console.log(result + "--- libc.so->recvfrom: " + hexdump(this.arg1, { length: size }))
            }
            LogPrint("leave libc.so->recvfrom");
        }
    });

    Interceptor.attach(sendto_addr, {
        onEnter: function (args) {
            this.arg0 = args[0];
            this.arg1 = args[1];
            this.arg2 = args[2];
            this.arg3 = args[3];
            this.arg4 = args[4];
            this.arg5 = args[5];

            LogPrint("go into libc.so->sendto");

            var result = getsocketdetail(this.arg0.toInt32());
            if (result.indexOf("udp") > 0) {
                handleUdp('sendto', this.arg4, this.arg5);
            }

            printNativeStack(this.context, "sendto");
        },
        onLeave: function (retval) {
            var size = this.arg2.toInt32();
            if (size > 0) {
                var result = getsocketdetail(this.arg0.toInt32());
                // console.log(`${result} --- libc.so->sendto: ${hexdump(this.arg1, { length: size })}`);
                console.log(result + "--- libc.so->sendto: " + hexdump(this.arg1, { length: size }))
            }
            LogPrint("leave libc.so->sendto");
        }
    });
}

function main() {
    hooktcp();
    hooklibc();
}

setImmediate(main)

抓包:目标App是我们前面说的测试用App

frida -UF -l hooksocket.js --no-pause -o log.txt

测试结果:

  • 根据hooklibc()调用printNativeStack()打印的结果来看,App使用的都是Java层的函数,并没有直接调用libc或者更深层的函数。
  • 这样就不需要hooklibc了,可以在脚本中删除这个函数了

修改脚本后继续测试:输入账号密码,点击登陆

  • 可以看到发送的数据,看起来像是base64加密的

  • 以及发包函数a.a.a.a$a以及收包函数a.a.a.a$c,这里的调试信息也没有抹去,可以定位到它的地址在214行

  • 以及java.lang.Thread.run的地址在764行

用GDA分析apk

  • 有360的壳

那就先脱壳,在协议分析中很多时候都要先脱壳才能分析:这里脱壳要使用getDex所以刷了7.1.2

  • 这里我们直接使用fart自动化脱壳工具进行脱壳就行了,360的壳直接去dump就可以了,因为它只是onCreate函数的一个vmp保护

编写脚本dump dex

// 只适用于存在getDex和getBytes api 的Android版本->7.1.2以下
function dumpdex(){
    // com.example.socket.MainActivity
    // a.a.a.a$a    // 直接通过类名定位到需要的dex
    Java.perform(function(){
        var File = Java.use("java.io.File")
        var FileOutputStream = Java.use("java.io.FileOutputStream")
        // 首先,定位到类的class loader,通过它得到class对象
        Java.enumerateClassLoadersSync().forEach(function(loader){
            console.log(loader + "\n")
            try {
                var aclass = loader.loadClass("a.a.a.a$a")
                console.log(aclass)
                var dexobj = aclass.getDex()
                console.log(dexobj)
                var dexbytes = dexobj.getBytes()
                var dexsavepath = "/data/data/com.example.socket/dump.dex"
                var dexfile = File.$new(dexsavepath)
                if(!dexfile.exists()){
                    dexfile.createNewFile()
                }
                var fileoutputstream = FileOutputStream.$new(dexsavepath)
                fileoutputstream.write(dexbytes)
                fileoutputstream.flush()
                fileoutputstream.close()
                console.log("save dex success!")
            } catch (err) {
                
            }
        })
    })
}
  • 在Android7以下,可以通过getDexgetBytes来获取目标dex并dump下来的方式进行脱壳

dump成功

  • 用GDA分析的时候提示文件打开失败
  • 检查一下是不是文件头错误,用010Edit把文件头替换成正常的文件头
  • 头没有了,八个字节全都是0
  • 替换个正常的头过来
  • 脱壳后就可以看到run函数
  • 合并两个参数
  • hook HelloKitty.hello获取参数和返回值

    function hookHelloKitty(){
        // com.example.socket.MainActivity
        // a.a.a.a$a    // 直接通过类名定位到需要的dex
        Java.perform(function(){
            var File = Java.use("java.io.File")
            var FileOutputStream = Java.use("java.io.FileOutputStream")
            // 首先,定位到类的class loader,通过它得到class对象
            Java.enumerateClassLoadersSync().forEach(function(loader){
                console.log(loader + "\n")
                try {
                    var aclass = loader.loadClass("a.a.a.a$a")
                    Java.classFactory.loader = loader
                    var HelloKitty = Java.use("com.example.socket.HelloKitty")
                    // hello
                    HelloKitty.hello.implementation = function(arg0){
                        console.log("HelloKitty.hello is called! arg0: " + arg0)
                        var result = this.hello(arg0)
                        console.log("HelloKitty.hello called over! result: " + result)
                        return result
                    }
                } catch (err) {
                    
                }
            })
        })
    }
    
    • 通过hook直接打印出来了我们输入的账号密码

这些是hook a.a.a.a$a也就是hello得到的结果,是发送

  • 这里的.c是接收相关的函数

查看a.a(byte)

  • kitty函数是一个native函数,暂时不管它

那么我们这里继续hook HelloKitty.kitty(str)函数来拿到它的参数和返回值

                // kitty
                HelloKitty.kitty.implementation = function (arg0) {
                    console.log("HelloKitty.kitty is called! arg0: " + arg0)
                    var result = this.kitty(arg0)
                    console.log("HelloKitty.kitty called over! result: " + result)
                    return result
                }

hook结果

  • 可以拿到传入的参数

  • kitty函数执行完之后解密的字符串

  • 还有点击登录响应之后返回的结果->提示密码错误

此时经过前面的hook我们基本可以知道:hello()是用来加密的,kitty()是用来解密的

这两个都是native函数,而这个360的壳没有对so进行加密,用IDA可以分析这个so

  • ida里面可以直接搜索到这两个函数

如果暴力破解的话

我们需要去模拟框起来的流程,模拟它的流程给服务端发送指定格式的数据,然后根据返回的结果判断是否破解成功

function checkuser(username) {
  //com.example.socket.MainActivity
  //a.a.a.a$a
  /*   String crypt = HelloKitty.hello(this.f1a);
                      byte[] content = crypt.getBytes("utf-8");
                      MessageDigest m = MessageDigest.getInstance("MD5");
                      m.update(crypt.getBytes("UTF8"));
                      m.digest()
                      byte[] finalarray = a.a(content, m.digest());
                      if (a.e != null) {
                          a.e.write(finalarray);
                      }

*/
  Java.perform(function () {
    var File = Java.use('java.io.File');
    var FileOutputStream = Java.use('java.io.FileOutputStream');
    Java.enumerateClassLoadersSync().forEach(function (loader) {
      console.log(loader + "\n");
      try {
        loader.loadClass("a.a.a.a$a")
        Java.classFactory.loader = loader;
        var HelloKitty = Java.use('com.example.socket.HelloKitty');

        var crypt = HelloKitty.hello(username);
        console.log("hello result:" + crypt);
        /*byte[] content = "utf-8";
    content = crypt.getBytes(content);*/

        var StringClass = Java.use('java.lang.String');
        console.log(StringClass)
        var strobj = StringClass.$new(crypt);
        console.log(strobj)
        var cryptbytes = strobj.getBytes("utf-8");
        console.log(JSON.stringify(cryptbytes));
        /*  MessageDigest m = MessageDigest.getInstance("MD5");
            m.update(crypt.getBytes("UTF8"));
            m.digest()*/
        var MessageDigest = Java.use('java.security.MessageDigest');
        var m = MessageDigest.getInstance("MD5");
        m.update(cryptbytes);
        var md5hash = m.digest();
        console.log("hash:" + JSON.stringify(md5hash));
        //byte[] finalarray = a.a(content, m.digest());
        //                         if (a.e != null) {
        //                             a.e.write(finalarray);
        //                         }

        var aClass = Java.use('a.a.a.a');
        console.log(aClass)
        var sendbytes = aClass.a(cryptbytes, md5hash);
        console.log("sendbytes:" + JSON.stringify(sendbytes));
        var outputstream = aClass._e.value;
        console.log(outputstream);
        outputstream.write(sendbytes);
        outputstream.flush();
        console.log("send success");
      } catch (e) {
      }
    })
  })
}
function main() {
  checkuser('{"msgtype":"login","loginname":"100","loginpwd":"qaz"}')
}

setImmediate(main);
  • 只要循环去生成0~100的账号和三个字母的密码,循环就可以了
  • 这种暴力破解只需要不停的传递参数模拟调用就可以了,不涉及对它native层的加解密的分析

未完待续。。。

  • 标题: 协议枚举、爆破及算法模拟
  • 作者: xiaoeryu
  • 创建于 : 2024-07-29 10:04:08
  • 更新于 : 2024-07-29 10:11:36
  • 链接: https://github.com/xiaoeryu/2024/07/29/协议枚举、爆破及算法模拟/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
此页目录
协议枚举、爆破及算法模拟