Frida

大约 15 分钟

Frida

Frida是一个安全逆向方向广泛使用的分析套件,Frida本身有一个FridaRPC模块,和sekiro提供类似的功能。不过一般情况这个rpc模块是通过USB连接线(或者在pc上,处理PC程序),然后将API能力暴露给python使用。

Frida RPC vs Sekiro Frida

sekiro在这一块虽然提供的是相似的功能,但是具备Sekiro自己的一些特点,如下:

  • 跨语言能力:SekiroFrida提供的RPC,可以作为http服务,以及给invoker做跨语言调用。FridaRPC则一般是提供给Python调用,如果需要支持跨语言,那么一般需要通过python的web服务将接口进行转发。
  • 公网发布:SekiroFrida提供的RPC接口,直接发布到服务器上,这样任何地方的服务器都可以调用他。而FridaRPC做到这一点还需要提供内网穿透的配置,这在实际的生产环境中将会非常容易不稳定。
  • 负载均衡:Sekiro天生具备多节点负载均衡能力,即可以挂载多个节点,实现多节点备份和负载均衡
  • 脱机:一般来说,使用FridaRPC模块处理Android/IOS等移动端app的时候,必须使用USB中转,所以无法做到脱机使用(这是因为他的api是python,Android中没有python环境)。但是sekiro则是直接和服务器通信,所以只需要实现frida自己的脱机,即可让RPC功能脱机
  • 服务控制:权限、限流、监控报表等管理功能
  • 高性能:这主要体现在Sekiro服务器的设计中,Sekiro服务器是一个高性能的NIO服务程序,可以支持非常强悍的网络吞吐。一般情况他是flask/Django的百倍性能(Sekiro最初来自余Django实现的web转发方案的剥离改造,经历过实践的高压力生产测试)

上手使用

注入如下Frida脚本

var SekiroClient=function(){function e(e){this.handlers={},this.isConnecting=!1,this.sekiroOption=e,e.serverHost=e.serverHost||"sekiro.iinti.cn",e.serverPort=e.serverPort||5612,this.fridaSocketConfig={family:"ipv4",host:e.serverHost,port:e.serverPort},console.log("           welcome to use sekiro framework,\n      for more support please visit our website: https://iinti.cn\n"),this.doConnect()}return e.prototype.registerAction=function(e,t){this.handlers[e]=t},e.prototype.reConnect=function(){var e=this;console.log("sekiro try connection after 5s"),setTimeout(function(){return e.doConnect()},5e3)},e.prototype.doConnect=function(){var e=this;this.isConnecting||(this.isConnecting=!0,console.log("sekiro connect to server-> "+this.fridaSocketConfig.host+":"+this.fridaSocketConfig.port),Socket.connect(this.fridaSocketConfig).then(function(t){e.isConnecting=!1,t.setNoDelay(!0),e.conn=t,e.connRead(),e.connWrite({type:16,serialNumber:-1,headers:{SEKIRO_GROUP:e.sekiroOption.sekiroGroup,SEKIRO_CLIENT_ID:e.sekiroOption.clientId}})})["catch"](function(t){e.isConnecting=!1,console.log("sekiro connect failed",t),e.reConnect()}))},e.prototype.connWrite=function(e){var t=this;this.conn.output.write(this.encodeSekiroPacket(e))["catch"](function(e){console.log("sekiro write register cmd failed",e),t.reConnect()})},e.prototype.connRead=function(){var e=this;this.conn.input.read(1024).then(function(t){return t.byteLength<=0?(e.conn.close(),console.log("sekiro server lost!"),void e.reConnect()):(e.onServerData(t),void setImmediate(function(){e.connRead()}))})["catch"](function(t){console.log("sekiro read_loop error",t),e.reConnect()})},e.prototype.onServerData=function(e){var t=this;if(this.readBuffer){if(e){var n=new ArrayBuffer(this.readBuffer.byteLength+e.byteLength),o=new Uint8Array(n);o.set(new Uint8Array(this.readBuffer),0),o.set(new Uint8Array(e),this.readBuffer.byteLength),this.readBuffer=n}}else{if(!e)return;this.readBuffer=e}var r=this.decodeSekiroPacket();r&&(this.handleServerPkg(r),setImmediate(function(){return t.onServerData()}))},e.prototype.encodeSekiroFastJSON=function(e){var t=void 0;e.msg&&(t=this.str2Uint8(e.msg));var n=void 0;e.data&&(n=this.str2Uint8(JSON.stringify(e.data)));var o=8+(t?t.length:0)+4+(n?n.length:0),r=new ArrayBuffer(o),i=new DataView(r);i.setInt32(0,e.status),i.setInt32(4,t?t.length:0);var s=8;return t&&(new Uint8Array(r,8).set(t),s+=t.length),i.setInt32(s,n?n.length:0),s+=4,n&&new Uint8Array(r,s).set(n),r},e.prototype.handleServerPkg=function(e){if(0==e.type)return void this.connWrite(e);if(32!=e.type)return void console.log("unknown server message:"+JSON.stringify(e));var t=this,n=function(n){t.connWrite({type:17,serialNumber:e.serialNumber,headers:{PAYLOAD_CONTENT_TYPE:"CONTENT_TYPE_SEKIRO_FAST_JSON"},data:t.encodeSekiroFastJSON(n)})},o={resolve:function(e){n({status:0,data:e})},reject:function(e){n({status:-1,msg:e})}};if(!e.data)return void o.reject("sekiro system error, no request payload present!!");var r=this.uint8toStr(new Uint8Array(e.data));console.log("sekiro receive request: "+r);var i=JSON.parse(r);if(!i.action)return void o.reject("the param: {action} not presented!!");var s=this.handlers[i.action];if(!s)return void o.reject("sekiro no handler for this action");try{s(i,o.resolve,o.reject)}catch(c){o.reject("sekiro handler error:"+c+JSON.stringify(c))}},e.prototype.decodeSekiroPacket=function(){if(!this.readBuffer)return void 0;var e=new DataView(this.readBuffer),t=e.getInt32(0),n=e.getInt32(4);if(1936026473!=t||1919889457!=n)return console.log("sekiro packet data"),this.conn.close().then(function(){console.log("sekiro close broken pipe")}),void(this.readBuffer=void 0);var o=e.getInt32(8);if(!(this.readBuffer.byteLength<o+12)){for(var r=e.getInt8(12),i=e.getInt32(13),s=e.getInt8(17),c=18,a={},f=0;s>f;f++){var u=e.getInt8(c++),h=this.uint8toStr(new Uint8Array(this.readBuffer.slice(c,u)));c+=u;var l=e.getInt8(c++),d="";l>0&&(d=this.uint8toStr(new Uint8Array(this.readBuffer.slice(c,l))),c+=l),a[h]=d}var p=void 0,y=o+12-c;return y>0&&(p=this.readBuffer.slice(c,c+y)),this.readBuffer.byteLength==o+12?this.readBuffer=void 0:this.readBuffer=this.readBuffer.slice(c),{type:r,serialNumber:i,headers:a,data:p}}},e.prototype.encodeSekiroPacket=function(e){var t=6,n=[];for(var o in e.headers)n.push(this.str2Uint8(o)),n.push(this.str2Uint8(e.headers[o])),t+=2;t+=n.reduce(function(e,t){return e+t.length},0),e.data&&(t+=e.data.byteLength);var r=new ArrayBuffer(t+12),i=new DataView(r);i.setUint32(0,1936026473),i.setUint32(4,1919889457),i.setInt32(8,t),i.setInt8(12,e.type),i.setInt32(13,e.serialNumber),i.setInt8(17,Object.keys(e.headers).length);var s=18;return n.forEach(function(e){i.setInt8(s++,e.length),new Uint8Array(r,s).set(e),s+=e.length}),e.data&&new Uint8Array(r,s).set(new Uint8Array(e.data)),r},e.prototype.uint8toStr=function(e){for(var t,n,o=0,r=Math.min(65536,e.length+1),i=new Uint16Array(r),s=[],c=0,a=function(){var a=o<e.length;if(!a||c>=r-1){var f=i.subarray(0,c),u=[];if(f.forEach(function(e){return u.push(e)}),s.push(String.fromCharCode.apply(null,u)),!a)return{value:s.join("")};e=e.subarray(o),o=0,c=0}var h=e[o++];if(0===(128&h))i[c++]=h;else if(192===(224&h))n=63&e[o++],i[c++]=(31&h)<<6|n;else if(224===(240&h))n=63&e[o++],t=63&e[o++],i[c++]=(31&h)<<12|n<<6|t;else if(240===(248&h)){n=63&e[o++],t=63&e[o++];var l=63&e[o++],d=(7&h)<<18|n<<12|t<<6|l;d>65535&&(d-=65536,i[c++]=d>>>10&1023|55296,d=56320|1023&d),i[c++]=d}};;){var f=a();if("object"==typeof f)return f.value}},e.prototype.str2Uint8=function(e){for(var t=0,n=e.length,o=0,r=Math.max(32,n+(n>>>1)+7),i=new Uint8Array(r>>>3<<3);n>t;){var s=e.charCodeAt(t++);if(s>=55296&&56319>=s){if(n>t){var c=e.charCodeAt(t);56320===(64512&c)&&(++t,s=((1023&s)<<10)+(1023&c)+65536)}if(s>=55296&&56319>=s)continue}if(o+4>i.length){r+=8,r*=1+t/e.length*2,r=r>>>3<<3;var a=new Uint8Array(r);a.set(i),i=a}if(0!==(4294967168&s)){if(0===(4294965248&s))i[o++]=s>>>6&31|192;else if(0===(4294901760&s))i[o++]=s>>>12&15|224,i[o++]=s>>>6&63|128;else{if(0!==(4292870144&s))continue;i[o++]=s>>>18&7|240,i[o++]=s>>>12&63|128,i[o++]=s>>>6&63|128}i[o++]=63&s|128}else i[o++]=s}return i.slice?i.slice(0,o):i.subarray(0,o)},e}();

var client = new SekiroClient({ sekiroGroup: "test_frida", clientId: "test" });
client.registerAction("testAction", function (request, resolve, reject) {
      resolve("ok");
});

访问FridaRPC链接:

https://sekiro.iinti.cn/business/invoke?group=test_frida&action=testAction&param=testparmopen in new window

API接口

指定服务连接参数

创建SekiroClient的时候,支持传入如下四个参数,分别为:服务器host、服务器port、clientId、sekiroGroup

interface SekiroOption {
    serverHost?: string;
    serverPort?: number;
    clientId: string;
    sekiroGroup: string;
}

如希望连接指定的Sekiro服务器:

var client = new SekiroClient({
    serverHost: "123.34.55.84", sekiroPort: 5612,
    sekiroGroup: "test_frida", clientId: "test"
});

Sekiro处理器

连接到sekiro服务器之后,需要接受服务器转发过来的参数,然后书写参数处理逻辑,并且返回处理结果。如下demo:

client.registerAction("testAction",
    function (request, resolve, reject) {
        resolve("ok");
    }
);
  • 调用registerAction,实现一个handler的注册
  • request代表的请求参数:如group=test_frida&action=testAction&param=testparm,则上述代码可以通过request获取到这些参数:console.log(request.param)
  • 返回成功数据:resolve(xxxx);其中xxxx代表你想返回的任意正确数据内容
  • 返回错误数据:reject("错误ddd");,reject函数调用需要是一个字符串。框架通过成功和失败在中心服务器提供简单的统计功能

内部机理

  • SekiroSDK使用Frida标准的socket接口进行编程: https://frida.re/docs/javascript-api/#socketopen in new window
  • 考虑JS本身是异步编程环境,SekiroHandler的执行过程也是异步的,也就是说我们没有开辟新的线程。如果您的API调用的时候本身存在多线程问题,那么需要您手动考虑。这是因为线程和平台强相关,Frida本身没有提供创建线程的能力(但是具体到不同的平台,如ios/Android,创建一个线程并不是麻烦事儿)
  • resolve函数的调用结果,需要是可以被javascript json序列化的,如果不是可能需要编程者手动干预
  • Frida不支持utf8编码,这里Sekiro自行实现了utf8编解码

sdk

由于Frida提供的是js的API,并且支持typescript,所以我这里提供了tsjs两种sdk,不过实际上js是来自ts的自动转换。

typescript

interface SekiroOption {
    serverHost?: string;
    serverPort?: number;
    clientId: string;
    sekiroGroup: string;
}

interface SekiroPacket {
    type: number;
    serialNumber: number;
    headers: any;
    data: ArrayBuffer | undefined;
}

/**
 * sekiro client base on frida socket api: https://frida.re/docs/javascript-api/#socket
 * sekiro socket internal protocol document: http://sekiro.iinti.cn/sekiro-doc/03_developer/1.protocol.html
 */
class SekiroClient {
    private readonly sekiroOption: SekiroOption;
    private readonly fridaSocketConfig: TcpConnectOptions;
    private handlers: any = {};
    private readBuffer?: ArrayBuffer | undefined;
    private isConnecting = false;

    constructor(sekiroOption: SekiroOption) {
        this.sekiroOption = sekiroOption;
        sekiroOption.serverHost = sekiroOption.serverHost || "sekiro.iinti.cn";
        sekiroOption.serverPort = sekiroOption.serverPort || 5612;
        this.fridaSocketConfig = {
            family: "ipv4",
            host: sekiroOption.serverHost,
            port: sekiroOption.serverPort
        }
        console.log("           welcome to use sekiro framework,\n" +
            "      for more support please visit our website: https://iinti.cn\n")
        this.doConnect();
    }

    public registerAction(action: string, handle: (request: any, resolve: (data: any) => void, reject: (msg: string) => void) => void) {
        this.handlers[action] = handle
    }

    private reConnect() {
        console.log("sekiro try connection after 5s",);
        setTimeout(() => this.doConnect(), 5000)
    }

    private doConnect() {
        if (this.isConnecting) {
            return
        }
        this.isConnecting = true;
        console.log("sekiro connect to server-> "
            + this.fridaSocketConfig.host + ":" + this.fridaSocketConfig.port
        );
        Socket.connect(this.fridaSocketConfig)
            .then((connection: SocketConnection) => {
                this.isConnecting = false;
                connection.setNoDelay(true)// no delay, sekiro packet has complement message block
                    .finally(() => {
                        this.connRead(connection);
                        this.connWrite(connection, {
                            type: 0x10,
                            serialNumber: -1,
                            headers: {
                                'SEKIRO_GROUP': this.sekiroOption.sekiroGroup,
                                'SEKIRO_CLIENT_ID': this.sekiroOption.clientId,
                            },
                        } as SekiroPacket)
                    })
            })
            .catch((reason: any) => {
                this.isConnecting = false;
                console.log("sekiro connect failed", reason);
                this.reConnect();
            });
    }

    private connWrite(conn: SocketConnection, sekiroPacket: SekiroPacket) {
        conn.output.write(this.encodeSekiroPacket(sekiroPacket))
            .catch((reason: any) => {
                console.log("sekiro write register cmd failed", reason);
                this.reConnect();
            })
    }

    private connRead(conn: SocketConnection) {
        conn.input.read(1024)
            .then((buffer: ArrayBuffer) => {
                if (buffer.byteLength <= 0) {
                    conn.close().finally(() => {
                        console.log("sekiro server lost!");
                        this.reConnect();
                    });
                    return;
                }
                this.onServerData(conn, buffer);
                setImmediate(() => {
                    this.connRead(conn);
                });
            })
            .catch((reason: any) => {
                console.log("sekiro read_loop error", reason);
                this.reConnect();
            });
    }

    private onServerData(conn: SocketConnection, buffer?: ArrayBuffer) {
        // merge buffer data
        if (!this.readBuffer) {
            if (!buffer) {
                return;
            }
            this.readBuffer = buffer;
        } else if (!!buffer) {
            const merge = new ArrayBuffer(this.readBuffer.byteLength + buffer.byteLength);
            const view = new Uint8Array(merge);
            view.set(new Uint8Array(this.readBuffer), 0);
            view.set(new Uint8Array(buffer), this.readBuffer.byteLength);
            this.readBuffer = merge;
        }
        const pkt = this.decodeSekiroPacket(conn);
        if (!!pkt) {
            this.handleServerPkg(conn, pkt);
            //maybe more data can be decoded
            setImmediate(() => this.onServerData(conn));
        }
    }

    private encodeSekiroFastJSON(commonRes: any): ArrayBuffer {
        let msgPart: Uint8Array | undefined = undefined;
        if (commonRes.msg) {
            msgPart = this.str2Uint8(commonRes.msg)
        }
        let jsonPart: Uint8Array | undefined = undefined;
        if (commonRes.data) {
            jsonPart = this.str2Uint8(JSON.stringify(commonRes.data))
        }
        let contentLen = 4 + 4 + (msgPart ? msgPart.length : 0)
            + 4 + (jsonPart ? jsonPart.length : 0);
        const arrayBuffer = new ArrayBuffer(contentLen);
        const v = new DataView(arrayBuffer);
        v.setInt32(0, commonRes.status);
        v.setInt32(4, msgPart ? msgPart.length : 0);
        let cursor = 8;
        if (msgPart) {
            new Uint8Array(arrayBuffer, 8).set(msgPart);
            cursor += msgPart.length;
        }
        v.setInt32(cursor, jsonPart ? jsonPart.length : 0);
        cursor += 4;
        if (jsonPart) {
            new Uint8Array(arrayBuffer, cursor).set(jsonPart);
        }
        return arrayBuffer;
    }

    private handleServerPkg(conn: SocketConnection, pkt: SekiroPacket): void {
        if (pkt.type == 0x00) {
            // this is heart beat pkg
            this.connWrite(conn, pkt);
            return;
        }
        if (pkt.type != 0x20) {
            console.log("unknown server message:" + JSON.stringify(pkt));
            return;
        }

        const that = this;
        let writeInvokeResponse = (json: any) => {
            setImmediate(() => {
                that.connWrite(conn, {
                    type: 0x11, serialNumber: pkt.serialNumber,
                    headers: {"PAYLOAD_CONTENT_TYPE": "CONTENT_TYPE_SEKIRO_FAST_JSON"},
                    data: that.encodeSekiroFastJSON(json)
                });
            });
        };
        let resolve = (data) => {
            writeInvokeResponse({status: 0, data: data});
        }
        let reject = (msg: string) => {
            writeInvokeResponse({status: -1, msg: msg});
        }

        if (!pkt.data) {
            reject("sekiro system error, no request payload present!!");
            return;
        }
        let requestStr = this.uint8toStr(new Uint8Array(pkt.data));
        console.log("sekiro receive request: " + requestStr);
        const requestJSON = JSON.parse(requestStr);

        if (!requestJSON['action']) {
            reject("the param: {action} not presented!!");
            return;
        }
        const handler = this.handlers[requestJSON['action']] as (request: any, resolve: (data: any) => void, reject: (msg: string) => void) => void
        if (!handler) {
            reject("sekiro no handler for this action");
            return;
        }
        try {
            handler(requestJSON, resolve, reject);
        } catch (e) {
            reject("sekiro handler error:" + e + JSON.stringify(e));
        }
    }

    private decodeSekiroPacket(conn: SocketConnection): SekiroPacket | undefined {
        if (!this.readBuffer) {
            return undefined;
        }
        let v = new DataView(this.readBuffer);
        const magic1 = v.getInt32(0);
        const magic2 = v.getInt32(4);
        if (magic1 != 0x73656b69 || magic2 != 0x726f3031) {
            console.log("sekiro packet data");
            conn.close().then(() => {
                console.log("sekiro close broken pipe");
            })
            this.readBuffer = undefined;
            return;
        }

        const pkgLength = v.getInt32(8);
        if (this.readBuffer.byteLength < pkgLength + 12) {
            return;// not enough data,wait next read event
        }

        let type = v.getInt8(12)
        let seq = v.getInt32(13);
        const headerSize = v.getInt8(17);
        let cursor = 18;
        let headers: any = {};
        for (let i = 0; i < headerSize; i++) {
            const keyLen = v.getInt8(cursor++);
            let key = this.uint8toStr(new Uint8Array(this.readBuffer.slice(cursor, keyLen)));
            cursor += keyLen;

            const valueLen = v.getInt8(cursor++);
            let value = "";
            if (valueLen > 0) {
                value = this.uint8toStr(new Uint8Array(this.readBuffer.slice(cursor, valueLen)));
                cursor += valueLen;
            }

            headers[key] = value;
        }

        let data: ArrayBuffer | undefined = undefined;
        let dataPayloadLen = (pkgLength + 12 - cursor);
        if (dataPayloadLen > 0) {
            data = this.readBuffer.slice(cursor, cursor + dataPayloadLen);
        }
        if (this.readBuffer.byteLength == pkgLength + 12) {
            this.readBuffer = undefined;
        } else {
            this.readBuffer = this.readBuffer.slice(cursor);
        }
        return {
            type,
            serialNumber: seq,
            headers,
            data
        };
    }

    private encodeSekiroPacket(sekiroPacket: SekiroPacket): ArrayBuffer {
        let num = 6; // 1 + 4 + 1
        let headerList = [];
        for (let h in sekiroPacket.headers) {
            headerList.push(this.str2Uint8(h));
            headerList.push(this.str2Uint8(sekiroPacket.headers[h]));
            num += 2;
        }
        num += headerList.reduce((res, it) => res + it.length, 0);
        if (sekiroPacket.data) {
            num += sekiroPacket.data.byteLength;
        }

        let buffer = new ArrayBuffer(num + 12);
        let dataView = new DataView(buffer);
        dataView.setUint32(0, 0x73656b69); // seki
        dataView.setUint32(4, 0x726f3031); // ro01
        dataView.setInt32(8, num); // payload length
        dataView.setInt8(12, sekiroPacket.type); // 1 pkg type
        dataView.setInt32(13, sekiroPacket.serialNumber); // 4 seq
        dataView.setInt8(17, Object.keys(sekiroPacket.headers).length); //1

        let cursor = 18;
        headerList.forEach((header: Uint8Array) => {
            dataView.setInt8(cursor++, header.length);
            new Uint8Array(buffer, cursor).set(header);
            cursor += header.length;
        })
        if (sekiroPacket.data) {
            new Uint8Array(buffer, cursor).set(new Uint8Array(sekiroPacket.data));
        }
        return buffer;
    }

    // the frida js runtime do not support TextEncoder/TextDecoder to handle transfer between ArrayBuffer and string
    // in node: Buffer.from(string)
    // in browser: encodeURLComponent or XHR with blob
    // this component extracted from https://github.com/samthor/fast-text-encoding/blob/master/src/lowlevel.js
    private uint8toStr(bytes: Uint8Array): string {
        let byte3;
        let byte2;
        let inputIndex = 0;

        const pendingSize = Math.min(256 * 256, bytes.length + 1);
        const pending = new Uint16Array(pendingSize);
        const chunks = [];
        let pendingIndex = 0;

        for (; ;) {
            const more = inputIndex < bytes.length;
            if (!more || (pendingIndex >= pendingSize - 1)) {
                const subarray = pending.subarray(0, pendingIndex);

                let temp: number[] = [];
                subarray.forEach((item) => temp.push(item));
                chunks.push(String.fromCharCode.apply(null, temp));

                if (!more) {
                    return chunks.join('');
                }
                bytes = bytes.subarray(inputIndex);
                inputIndex = 0;
                pendingIndex = 0;
            }
            const byte1 = bytes[inputIndex++];
            if ((byte1 & 0x80) === 0) {  // 1-byte or null
                pending[pendingIndex++] = byte1;
            } else if ((byte1 & 0xe0) === 0xc0) {  // 2-byte
                byte2 = bytes[inputIndex++] & 0x3f;
                pending[pendingIndex++] = ((byte1 & 0x1f) << 6) | byte2;
            } else if ((byte1 & 0xf0) === 0xe0) {  // 3-byte
                byte2 = bytes[inputIndex++] & 0x3f;
                byte3 = bytes[inputIndex++] & 0x3f;
                pending[pendingIndex++] = ((byte1 & 0x1f) << 12) | (byte2 << 6) | byte3;
            } else if ((byte1 & 0xf8) === 0xf0) {  // 4-byte
                byte2 = bytes[inputIndex++] & 0x3f;
                byte3 = bytes[inputIndex++] & 0x3f;
                const byte4 = bytes[inputIndex++] & 0x3f;

                // this can be > 0xffff, so possibly generate surrogates
                let codepoint = ((byte1 & 0x07) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4;
                if (codepoint > 0xffff) {
                    // codepoint &= ~0x10000;
                    codepoint -= 0x10000;
                    pending[pendingIndex++] = (codepoint >>> 10) & 0x3ff | 0xd800;
                    codepoint = 0xdc00 | codepoint & 0x3ff;
                }
                pending[pendingIndex++] = codepoint;
            } else {
                // invalid initial byte
            }
        }
    }


    private str2Uint8(string: any): Uint8Array {
        let pos = 0;
        const len = string.length;

        let at = 0;  // output position
        let tlen = Math.max(32, len + (len >>> 1) + 7);  // 1.5x size
        let target = new Uint8Array((tlen >>> 3) << 3);  // ... but at 8 byte offset

        while (pos < len) {
            let value = string.charCodeAt(pos++);
            if (value >= 0xd800 && value <= 0xdbff) {
                // high surrogate
                if (pos < len) {
                    var extra = string.charCodeAt(pos);
                    if ((extra & 0xfc00) === 0xdc00) {
                        ++pos;
                        value = ((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000;
                    }
                }
                if (value >= 0xd800 && value <= 0xdbff) {
                    continue;  // drop lone surrogate
                }
            }

            // expand the buffer if we couldn't write 4 bytes
            if (at + 4 > target.length) {
                tlen += 8;  // minimum extra
                tlen *= (1.0 + (pos / string.length) * 2);  // take 2x the remaining
                tlen = (tlen >>> 3) << 3;  // 8 byte offset

                const update = new Uint8Array(tlen);
                update.set(target);
                target = update;
            }

            if ((value & 0xffffff80) === 0) {  // 1-byte
                target[at++] = value;  // ASCII
                continue;
            } else if ((value & 0xfffff800) === 0) {  // 2-byte
                target[at++] = ((value >>> 6) & 0x1f) | 0xc0;
            } else if ((value & 0xffff0000) === 0) {  // 3-byte
                target[at++] = ((value >>> 12) & 0x0f) | 0xe0;
                target[at++] = ((value >>> 6) & 0x3f) | 0x80;
            } else if ((value & 0xffe00000) === 0) {  // 4-byte
                target[at++] = ((value >>> 18) & 0x07) | 0xf0;
                target[at++] = ((value >>> 12) & 0x3f) | 0x80;
                target[at++] = ((value >>> 6) & 0x3f) | 0x80;
            } else {
                continue;  // out of range
            }
            target[at++] = (value & 0x3f) | 0x80;
        }
        return target.slice ? target.slice(0, at) : target.subarray(0, at);
    }
}

export default SekiroClient;

javascript

"use strict";
exports.__esModule = true;
/**
 * sekiro client base on frida socket api: https://frida.re/docs/javascript-api/#socket
 * sekiro socket internal protocol document: http://sekiro.iinti.cn/sekiro-doc/03_developer/1.protocol.html
 */
var SekiroClient = /** @class */ (function () {
    function SekiroClient(sekiroOption) {
        this.handlers = {};
        this.isConnecting = false;
        this.sekiroOption = sekiroOption;
        sekiroOption.serverHost = sekiroOption.serverHost || "sekiro.iinti.cn";
        sekiroOption.serverPort = sekiroOption.serverPort || 5612;
        this.fridaSocketConfig = {
            family: "ipv4",
            host: sekiroOption.serverHost,
            port: sekiroOption.serverPort
        };
        console.log("           welcome to use sekiro framework,\n" +
            "      for more support please visit our website: https://iinti.cn\n");
        this.doConnect();
    }

    SekiroClient.prototype.registerAction = function (action, handle) {
        this.handlers[action] = handle;
    };
    SekiroClient.prototype.reConnect = function () {
        var _this = this;
        console.log("sekiro try connection after 5s");
        setTimeout(function () {
            return _this.doConnect();
        }, 5000);
    };
    SekiroClient.prototype.doConnect = function () {
        var _this = this;
        if (this.isConnecting) {
            return;
        }
        this.isConnecting = true;
        console.log("sekiro connect to server-> "
            + this.fridaSocketConfig.host + ":" + this.fridaSocketConfig.port);
        Socket.connect(this.fridaSocketConfig)
            .then(function (connection) {
                _this.isConnecting = false;
                connection.setNoDelay(true); // no delay, sekiro packet has complement message block
                _this.connRead(connection);
                _this.connWrite(connection, {
                    type: 0x10,
                    serialNumber: -1,
                    headers: {
                        'SEKIRO_GROUP': _this.sekiroOption.sekiroGroup,
                        'SEKIRO_CLIENT_ID': _this.sekiroOption.clientId
                    }
                });
            })["catch"](function (reason) {
            _this.isConnecting = false;
            console.log("sekiro connect failed", reason);
            _this.reConnect();
        });
    };
    SekiroClient.prototype.connWrite = function (conn, sekiroPacket) {
        var _this = this;
        conn.output.write(this.encodeSekiroPacket(sekiroPacket))["catch"](function (reason) {
            console.log("sekiro write register cmd failed", reason);
            _this.reConnect();
        });
    };
    SekiroClient.prototype.connRead = function (conn) {
        var _this = this;
        conn.input.read(1024)
            .then(function (buffer) {
                if (buffer.byteLength <= 0) {
                    conn.close()["finally"](function () {
                        console.log("sekiro server lost!");
                        _this.reConnect();
                    });
                    return;
                }
                _this.onServerData(conn, buffer);
                setImmediate(function () {
                    _this.connRead(conn);
                });
            })["catch"](function (reason) {
            console.log("sekiro read_loop error", reason);
            _this.reConnect();
        });
    };
    SekiroClient.prototype.onServerData = function (conn, buffer) {
        var _this = this;
        // merge buffer data
        if (!this.readBuffer) {
            if (!buffer) {
                return;
            }
            this.readBuffer = buffer;
        } else if (!!buffer) {
            var merge = new ArrayBuffer(this.readBuffer.byteLength + buffer.byteLength);
            var view = new Uint8Array(merge);
            view.set(new Uint8Array(this.readBuffer), 0);
            view.set(new Uint8Array(buffer), this.readBuffer.byteLength);
            this.readBuffer = merge;
        }
        var pkt = this.decodeSekiroPacket(conn);
        if (!!pkt) {
            this.handleServerPkg(conn, pkt);
            //maybe more data can be decoded
            setImmediate(function () {
                return _this.onServerData(conn);
            });
        }
    };
    SekiroClient.prototype.encodeSekiroFastJSON = function (commonRes) {
        var msgPart = undefined;
        if (commonRes.msg) {
            msgPart = this.str2Uint8(commonRes.msg);
        }
        var jsonPart = undefined;
        if (commonRes.data) {
            jsonPart = this.str2Uint8(JSON.stringify(commonRes.data));
        }
        var contentLen = 4 + 4 + (msgPart ? msgPart.length : 0)
            + 4 + (jsonPart ? jsonPart.length : 0);
        var arrayBuffer = new ArrayBuffer(contentLen);
        var v = new DataView(arrayBuffer);
        v.setInt32(0, commonRes.status);
        v.setInt32(4, msgPart ? msgPart.length : 0);
        var cursor = 8;
        if (msgPart) {
            new Uint8Array(arrayBuffer, 8).set(msgPart);
            cursor += msgPart.length;
        }
        v.setInt32(cursor, jsonPart ? jsonPart.length : 0);
        cursor += 4;
        if (jsonPart) {
            new Uint8Array(arrayBuffer, cursor).set(jsonPart);
        }
        return arrayBuffer;
    };
    SekiroClient.prototype.handleServerPkg = function (conn, pkt) {
        if (pkt.type == 0x00) {
            // this is heart beat pkg
            this.connWrite(conn, pkt);
            return;
        }
        if (pkt.type != 0x20) {
            console.log("unknown server message:" + JSON.stringify(pkt));
            return;
        }
        var that = this;
        var writeInvokeResponse = function (json) {
            setImmediate(function () {
                that.connWrite(conn, {
                    type: 0x11, serialNumber: pkt.serialNumber,
                    headers: {"PAYLOAD_CONTENT_TYPE": "CONTENT_TYPE_SEKIRO_FAST_JSON"},
                    data: that.encodeSekiroFastJSON(json)
                });
            });
        };
        var resolve = function (data) {
            writeInvokeResponse({status: 0, data: data});
        };
        var reject = function (msg) {
            writeInvokeResponse({status: -1, msg: msg});
        };
        if (!pkt.data) {
            reject("sekiro system error, no request payload present!!");
            return;
        }
        var requestStr = this.uint8toStr(new Uint8Array(pkt.data));
        console.log("sekiro receive request: " + requestStr);
        var requestJSON = JSON.parse(requestStr);
        if (!requestJSON['action']) {
            reject("the param: {action} not presented!!");
            return;
        }
        var handler = this.handlers[requestJSON['action']];
        if (!handler) {
            reject("sekiro no handler for this action");
            return;
        }
        try {
            handler(requestJSON, resolve, reject);
        } catch (e) {
            reject("sekiro handler error:" + e + JSON.stringify(e));
        }
    };
    SekiroClient.prototype.decodeSekiroPacket = function (conn) {
        if (!this.readBuffer) {
            return undefined;
        }
        var v = new DataView(this.readBuffer);
        var magic1 = v.getInt32(0);
        var magic2 = v.getInt32(4);
        if (magic1 != 0x73656b69 || magic2 != 0x726f3031) {
            console.log("sekiro packet data");
            conn.close().then(function () {
                console.log("sekiro close broken pipe");
            });
            this.readBuffer = undefined;
            return;
        }
        var pkgLength = v.getInt32(8);
        if (this.readBuffer.byteLength < pkgLength + 12) {
            return; // not enough data,wait next read event
        }
        var type = v.getInt8(12);
        var seq = v.getInt32(13);
        var headerSize = v.getInt8(17);
        var cursor = 18;
        var headers = {};
        for (var i = 0; i < headerSize; i++) {
            var keyLen = v.getInt8(cursor++);
            var key = this.uint8toStr(new Uint8Array(this.readBuffer.slice(cursor, keyLen)));
            cursor += keyLen;
            var valueLen = v.getInt8(cursor++);
            var value = "";
            if (valueLen > 0) {
                value = this.uint8toStr(new Uint8Array(this.readBuffer.slice(cursor, valueLen)));
                cursor += valueLen;
            }
            headers[key] = value;
        }
        var data = undefined;
        var dataPayloadLen = (pkgLength + 12 - cursor);
        if (dataPayloadLen > 0) {
            data = this.readBuffer.slice(cursor, cursor + dataPayloadLen);
        }
        if (this.readBuffer.byteLength == pkgLength + 12) {
            this.readBuffer = undefined;
        } else {
            this.readBuffer = this.readBuffer.slice(cursor);
        }
        return {
            type: type,
            serialNumber: seq,
            headers: headers,
            data: data
        };
    };
    SekiroClient.prototype.encodeSekiroPacket = function (sekiroPacket) {
        var num = 6; // 1 + 4 + 1
        var headerList = [];
        for (var h in sekiroPacket.headers) {
            headerList.push(this.str2Uint8(h));
            headerList.push(this.str2Uint8(sekiroPacket.headers[h]));
            num += 2;
        }
        num += headerList.reduce(function (res, it) {
            return res + it.length;
        }, 0);
        if (sekiroPacket.data) {
            num += sekiroPacket.data.byteLength;
        }
        var buffer = new ArrayBuffer(num + 12);
        var dataView = new DataView(buffer);
        dataView.setUint32(0, 0x73656b69); // seki
        dataView.setUint32(4, 0x726f3031); // ro01
        dataView.setInt32(8, num); // payload length
        dataView.setInt8(12, sekiroPacket.type); // 1 pkg type
        dataView.setInt32(13, sekiroPacket.serialNumber); // 4 seq
        dataView.setInt8(17, Object.keys(sekiroPacket.headers).length); //1
        var cursor = 18;
        headerList.forEach(function (header) {
            dataView.setInt8(cursor++, header.length);
            new Uint8Array(buffer, cursor).set(header);
            cursor += header.length;
        });
        if (sekiroPacket.data) {
            new Uint8Array(buffer, cursor).set(new Uint8Array(sekiroPacket.data));
        }
        return buffer;
    };
    // the frida js runtime do not support TextEncoder/TextDecoder to handle transfer between ArrayBuffer and string
    // in node: Buffer.from(string)
    // in browser: encodeURLComponent or XHR with blob
    // this component extracted from https://github.com/samthor/fast-text-encoding/blob/master/src/lowlevel.js
    SekiroClient.prototype.uint8toStr = function (bytes) {
        var byte3;
        var byte2;
        var inputIndex = 0;
        var pendingSize = Math.min(256 * 256, bytes.length + 1);
        var pending = new Uint16Array(pendingSize);
        var chunks = [];
        var pendingIndex = 0;
        var _loop_1 = function () {
            var more = inputIndex < bytes.length;
            if (!more || (pendingIndex >= pendingSize - 1)) {
                var subarray = pending.subarray(0, pendingIndex);
                var temp_1 = [];
                subarray.forEach(function (item) {
                    return temp_1.push(item);
                });
                chunks.push(String.fromCharCode.apply(null, temp_1));
                if (!more) {
                    return {value: chunks.join('')};
                }
                bytes = bytes.subarray(inputIndex);
                inputIndex = 0;
                pendingIndex = 0;
            }
            var byte1 = bytes[inputIndex++];
            if ((byte1 & 0x80) === 0) { // 1-byte or null
                pending[pendingIndex++] = byte1;
            } else if ((byte1 & 0xe0) === 0xc0) { // 2-byte
                byte2 = bytes[inputIndex++] & 0x3f;
                pending[pendingIndex++] = ((byte1 & 0x1f) << 6) | byte2;
            } else if ((byte1 & 0xf0) === 0xe0) { // 3-byte
                byte2 = bytes[inputIndex++] & 0x3f;
                byte3 = bytes[inputIndex++] & 0x3f;
                pending[pendingIndex++] = ((byte1 & 0x1f) << 12) | (byte2 << 6) | byte3;
            } else if ((byte1 & 0xf8) === 0xf0) { // 4-byte
                byte2 = bytes[inputIndex++] & 0x3f;
                byte3 = bytes[inputIndex++] & 0x3f;
                var byte4 = bytes[inputIndex++] & 0x3f;
                // this can be > 0xffff, so possibly generate surrogates
                var codepoint = ((byte1 & 0x07) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4;
                if (codepoint > 0xffff) {
                    // codepoint &= ~0x10000;
                    codepoint -= 0x10000;
                    pending[pendingIndex++] = (codepoint >>> 10) & 0x3ff | 0xd800;
                    codepoint = 0xdc00 | codepoint & 0x3ff;
                }
                pending[pendingIndex++] = codepoint;
            } else {
                // invalid initial byte
            }
        };
        for (; ;) {
            var state_1 = _loop_1();
            if (typeof state_1 === "object")
                return state_1.value;
        }
    };
    SekiroClient.prototype.str2Uint8 = function (string) {
        var pos = 0;
        var len = string.length;
        var at = 0; // output position
        var tlen = Math.max(32, len + (len >>> 1) + 7); // 1.5x size
        var target = new Uint8Array((tlen >>> 3) << 3); // ... but at 8 byte offset
        while (pos < len) {
            var value = string.charCodeAt(pos++);
            if (value >= 0xd800 && value <= 0xdbff) {
                // high surrogate
                if (pos < len) {
                    var extra = string.charCodeAt(pos);
                    if ((extra & 0xfc00) === 0xdc00) {
                        ++pos;
                        value = ((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000;
                    }
                }
                if (value >= 0xd800 && value <= 0xdbff) {
                    continue; // drop lone surrogate
                }
            }
            // expand the buffer if we couldn't write 4 bytes
            if (at + 4 > target.length) {
                tlen += 8; // minimum extra
                tlen *= (1.0 + (pos / string.length) * 2); // take 2x the remaining
                tlen = (tlen >>> 3) << 3; // 8 byte offset
                var update = new Uint8Array(tlen);
                update.set(target);
                target = update;
            }
            if ((value & 0xffffff80) === 0) { // 1-byte
                target[at++] = value; // ASCII
                continue;
            } else if ((value & 0xfffff800) === 0) { // 2-byte
                target[at++] = ((value >>> 6) & 0x1f) | 0xc0;
            } else if ((value & 0xffff0000) === 0) { // 3-byte
                target[at++] = ((value >>> 12) & 0x0f) | 0xe0;
                target[at++] = ((value >>> 6) & 0x3f) | 0x80;
            } else if ((value & 0xffe00000) === 0) { // 4-byte
                target[at++] = ((value >>> 18) & 0x07) | 0xf0;
                target[at++] = ((value >>> 12) & 0x3f) | 0x80;
                target[at++] = ((value >>> 6) & 0x3f) | 0x80;
            } else {
                continue; // out of range
            }
            target[at++] = (value & 0x3f) | 0x80;
        }
        return target.slice ? target.slice(0, at) : target.subarray(0, at);
    };
    return SekiroClient;
}());
exports["default"] = SekiroClient;
上次编辑于:
贡献者: liguobao