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¶m=testparm
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¶m=testparm
,则上述代码可以通过request
获取到这些参数:console.log(request.param)
- 返回成功数据:
resolve(xxxx);
其中xxxx代表你想返回的任意正确数据内容 - 返回错误数据:
reject("错误ddd");
,reject函数调用需要是一个字符串。框架通过成功和失败在中心服务器提供简单的统计功能
内部机理
- SekiroSDK使用Frida标准的socket接口进行编程: https://frida.re/docs/javascript-api/#socket
- 考虑JS本身是异步编程环境,SekiroHandler的执行过程也是异步的,也就是说我们没有开辟新的线程。如果您的API调用的时候本身存在多线程问题,那么需要您手动考虑。这是因为线程和平台强相关,Frida本身没有提供创建线程的能力(但是具体到不同的平台,如ios/Android,创建一个线程并不是麻烦事儿)
- resolve函数的调用结果,需要是可以被javascript json序列化的,如果不是可能需要编程者手动干预
- Frida不支持utf8编码,这里Sekiro自行实现了utf8编解码
sdk
由于Frida提供的是js的API,并且支持typescript
,所以我这里提供了ts
和js
两种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;