From 2e1d761e2857ca4d342ead6e269c1987d6d87702 Mon Sep 17 00:00:00 2001 From: DavidVentura Date: Fri, 26 Jan 2024 17:21:38 +0100 Subject: [PATCH] refactor code --- Makefile | 2 +- handlers.ts | 136 +++++++++++++++++++++++++++ hexdump.js | 105 +++++++++++++++++++++ impl.ts | 252 +++++++------------------------------------------- proto_diag.md | 2 +- server.ts | 102 ++++++++++++++++++++ shim.ts | 6 -- 7 files changed, 380 insertions(+), 225 deletions(-) create mode 100644 handlers.ts create mode 100644 hexdump.js create mode 100644 server.ts diff --git a/Makefile b/Makefile index f1aacfe..a889ec4 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ venv: requirements.txt touch venv run: node_modules - ./node_modules/.bin/ts-node --esm impl.ts + ./node_modules/.bin/ts-node --esm server.ts hook: bundle.js venv ./venv/bin/python3 -u loader3.py diff --git a/handlers.ts b/handlers.ts new file mode 100644 index 0000000..a628cb8 --- /dev/null +++ b/handlers.ts @@ -0,0 +1,136 @@ +import { sock } from "./server.js"; +import { Commands, CommandsByValue } from "./func_replacements.js"; +import { createWriteStream } from "node:fs"; +import { SendUsrAck, SendUsrChk, create_P2pRdy } from "./impl.js"; + +let image_fds = []; +let cur_image_index = 0; +let size_so_far = 0; + +export const notImpl = (_: sock, dv: DataView) => { + const raw = dv.readU16(); + const cmd = CommandsByValue[raw]; + console.log(`^^ ${cmd} (${raw.toString(16)}) and it's not implemented yet`); +}; + +export const noop = (_: sock, __: DataView) => {}; +const create_P2pAliveAck = (): DataView => { + const outbuf = new DataView(new Uint8Array(4).buffer); + outbuf.writeU16(Commands.P2PAliveAck); + outbuf.add(2).writeU16(0); + return outbuf; +}; + +export const handle_P2PAlive = (sock: sock, _: DataView) => { + const b = create_P2pAliveAck(); + sock.send(b); +}; +export const handle_PunchPkt = (sock: sock, dv: DataView) => { + const punchCmd = dv.readU16(); + const len = dv.add(2).readU16(); + const prefix = dv.add(4).readString(4); + const serial = dv.add(8).readU64().toString(); + const suffix = dv.add(16).readString(4); + // f141 20 BATC 609531 EXLV + sock.send(create_P2pRdy(dv.add(4).readByteArray(len))); +}; + +const deal_with_control = (sock: sock, dv: DataView) => { + const start_type = dv.add(8).readU16(); // 0xa11 on control; data starts here on DATA pkt + const cmd_id = dv.add(10).readU16(); // 0x1120 + + if (start_type != 0x110a) { + console.error(`Expected start_type to be 0xa11, got 0x${start_type.toString(16)}`); + return; + } + + if (cmd_id == ControlCommands.ConnectUserAck) { + /* + 00000000 f1 d0 00 18 d1 00 00 00 11 0a 20 11 0c 00 ff 00 .......... ..... + 00000010 00 00 00 00 34 54 63 4d fe 01 01 01 ....4TcM.... + ^^^^^^^^^^^ + some kind of challenge + need to send the 0x3010 + command with these 4 bytes 'encrypted' + */ + let challenge = [0, 0, 0, 0]; + challenge[0] = dv.add(0x14).readU8(); + challenge[1] = dv.add(0x15).readU8(); + challenge[2] = dv.add(0x16).readU8(); + challenge[3] = dv.add(0x17).readU8(); + const buf = SendUsrAck(challenge); + sock.send(buf); + } +}; + +const deal_with_data = (dv: DataView) => { + const pkt_len = dv.add(2).readU16(); + // data + const JPEG_HEADER = [0xff, 0xd8, 0xff, 0xdb]; + const AUDIO_HEADER = [0x55, 0xaa, 0x15, 0xa8]; + const m_hdr = dv.add(8).readByteArray(4); + let is_new_image = true; + let audio = true; + for (let i = 0; i < 4; i++) { + is_new_image = is_new_image && m_hdr.add(i).readU8() == JPEG_HEADER[i]; + audio = audio && m_hdr.add(i).readU8() == AUDIO_HEADER[i]; + } + + if (audio) { + // TODO audio pkt + } else { + if (is_new_image) { + size_so_far = 0; + if (cur_image_index > 0) { + image_fds[cur_image_index - 1].close(); + } + const fname = `captures/${cur_image_index.toString().padStart(4, "0")}.jpg`; + let cur_image = createWriteStream(fname); + cur_image.cork(); + image_fds[cur_image_index] = cur_image; + cur_image_index++; + } + + const data = dv.add(8).readByteArray(pkt_len - 4); + image_fds[cur_image_index - 1].write(Buffer.from(data.buffer)); + size_so_far += pkt_len - 4; + } +}; + +const ControlCommands = { + ConnectUser: 0x2010, + ConnectUserAck: 0x2011, +}; + +const makeDrwAck = (dv: DataView): DataView => { + const pkt_id = dv.add(6).readU16(); + const m_stream = dv.add(5).readU8(); // data = 1, control = 0 + const item_count = 1; // TODO coalesce acks + const reply_len = item_count * 2 + 4; // 4 hdr, 2b per item + const outbuf = new DataView(new Uint8Array(32).buffer); + outbuf.writeU16(Commands.DrwAck); + outbuf.add(2).writeU16(reply_len); + outbuf.add(4).writeU8(0xd2); + outbuf.add(5).writeU8(m_stream); + outbuf.add(6).writeU16(item_count); + for (let i = 0; i < item_count; i++) { + outbuf.add(8 + i * 2).writeU16(pkt_id); + } + return outbuf; +}; +export const handle_Drw = (sock: sock, dv: DataView) => { + const ack = makeDrwAck(dv); + sock.send(ack); + + const m_stream = dv.add(5).readU8(); // data = 1, control = 0 + if (m_stream == 1) { + deal_with_data(dv); + } else { + deal_with_control(sock, dv); + } +}; + +export const handle_P2PRdy = (sock: sock, _: DataView) => { + const b = SendUsrChk("admin", "admin"); + sock.send(b); +}; diff --git a/hexdump.js b/hexdump.js new file mode 100644 index 0000000..a4a328f --- /dev/null +++ b/hexdump.js @@ -0,0 +1,105 @@ +// hacked hexdump from frida to work on normal ArrayBuffers +import "./shim.ts"; +export const hexdump = (target, options) => { + options = options || {}; + + const startOffset = options.offset || 0; + let length = options.length; + const showHeader = options.hasOwnProperty("header") ? options.header : true; + const useAnsi = options.hasOwnProperty("ansi") ? options.ansi : false; + const ansiColor = options.hasOwnProperty("ansiColor") ? options.ansiColor : 0; + + let buffer; + if (target instanceof ArrayBuffer) { + if (length === undefined) length = target.byteLength; + else length = Math.min(length, target.byteLength); + buffer = target; + } else { + throw "Gimme array buffer"; + } + + const startAddress = 0; + const endAddress = target.byteLength; + + const bytes = new Uint8Array(buffer); + + const columnPadding = " "; + const leftColumnWidth = Math.max(endAddress.toString(16).length, 8); + const hexLegend = " 0 1 2 3 4 5 6 7 8 9 A B C D E F"; + const asciiLegend = "0123456789ABCDEF"; + + let resetColor, offsetColor, dataColor, newlineColor; + if (useAnsi) { + resetColor = "\x1b[0m"; + offsetColor = "\x1b[0;32m"; + dataColor = ansiColor == 0 ? "\x1b[0;33m" : "\x1b[0;34m"; + newlineColor = resetColor; + } else { + resetColor = ""; + offsetColor = ""; + dataColor = ""; + newlineColor = ""; + } + + const result = []; + + if (showHeader) { + result.push(pad(" ", leftColumnWidth, " "), columnPadding, hexLegend, columnPadding, asciiLegend, "\n"); + } + + let offset = startOffset; + for (let bufferOffset = 0; bufferOffset < length; bufferOffset += 16) { + if (bufferOffset !== 0) result.push("\n"); + + result.push( + offsetColor, + pad((startAddress + offset).toString(16), leftColumnWidth, "0"), + resetColor, + columnPadding, + ); + + const asciiChars = []; + const lineSize = Math.min(length - offset, 16); + + for (let lineOffset = 0; lineOffset !== lineSize; lineOffset++) { + const value = bytes[offset++]; + + const isNewline = value === 10; + + const hexPair = pad(value.toString(16), 2, "0"); + if (lineOffset !== 0) result.push(" "); + result.push(isNewline ? newlineColor : dataColor, hexPair, resetColor); + + asciiChars.push( + isNewline ? newlineColor : dataColor, + value >= 32 && value <= 126 ? String.fromCharCode(value) : ".", + resetColor, + ); + } + + for (let lineOffset = lineSize; lineOffset !== 16; lineOffset++) { + result.push(" "); + asciiChars.push(" "); + } + + result.push(columnPadding); + + Array.prototype.push.apply(result, asciiChars); + } + + let trailingSpaceCount = 0; + for (let tailOffset = result.length - 1; tailOffset >= 0 && result[tailOffset] === " "; tailOffset--) { + trailingSpaceCount++; + } + + return result.slice(0, result.length - trailingSpaceCount).join(""); +}; + +function pad(str, width, fill) { + const result = []; + const paddingSize = Math.max(width - str.length, 0); + for (let index = 0; index !== paddingSize; index++) { + result.push(fill); + } + return result.join("") + str; +} diff --git a/impl.ts b/impl.ts index 4716873..51530bd 100644 --- a/impl.ts +++ b/impl.ts @@ -1,9 +1,6 @@ import "./shim.ts"; -import dgram from "node:dgram"; - -import { Commands, CommandsByValue, create_LanSearch, XqBytesEnc } from "./func_replacements.js"; -import { hexdump } from "./hexdump.js"; +import { Commands, XqBytesEnc } from "./func_replacements.js"; import { u16_swap } from "./utils.js"; const str2byte = (s: string): number[] => { @@ -29,6 +26,39 @@ const DrwHdr = (cmd: number, len: number, d1_or_d2: 0xd1 | 0xd2, m_chan: number) return retret; }; +export const SendUsrAck = (challenge: number[]): DataView => { + // TODO: extract SendUsrChk + let buf = new DataView(new Uint8Array(0x18).buffer); + const seq = 0x1; + let bytes = [ + 0xf1, + 0xd0, + 0x01, + 0x14, + 0xd1, + 0x00, + 0x00, + seq, + 0x11, + 0x0a, + 0x10, + 0x30, + 0x08, + 0x01, + 0x00, + 0x00, + challenge[0] % 2 == 0 ? challenge[0] + 1 : challenge[0] - 1, + challenge[1] % 2 == 0 ? challenge[1] + 1 : challenge[1] - 1, + challenge[2] % 2 == 0 ? challenge[2] + 1 : challenge[2] - 1, + challenge[3] % 2 == 0 ? challenge[3] + 1 : challenge[3] - 1, + 0x01, + 0x01, + 0x01, + 0x01, + ]; + buf.writeByteArray(bytes); + return buf; +}; export const SendUsrChk = (username: string, password: string): DataView => { // type is char account[0x20]; char password[0x80]; let buf = new Uint8Array(0x20 + 0x80); @@ -55,60 +85,7 @@ export const SendUsrChk = (username: string, password: string): DataView => { return retret; }; -const EstablishSession = () => { - const ls = create_LanSearch(); // Broadcast -}; - -type sock = { send: (msg: DataView) => void; broadcast: (msg: DataView) => void }; -const MakeSock = (cb: (msg: Buffer, rinfo: any) => void): sock => { - const server = dgram.createSocket("udp4"); - - server.on("error", (err) => { - console.error(`server error:\n${err.stack}`); - server.close(); - }); - - server.on("message", cb); - - server.on("listening", () => { - const address = server.address(); - console.log(`server listening ${address.address}:${address.port}`); - server.setBroadcast(true); - }); - - const RECV_PORT = 49512; // important? - const DST_IP = "192.168.1.1"; - const BCAST_IP = "192.168.1.255"; - const SEND_PORT = 32108; - server.bind(RECV_PORT); - - return { - send: (msg: DataView) => { - const raw = msg.readU16(); - const cmd = CommandsByValue[raw]; - console.log(`>> ${cmd}`); - console.log(hexdump(msg.buffer, { ansi: true, ansiColor: 0 })); - server.send(new Uint8Array(msg.buffer), SEND_PORT, DST_IP); - }, - broadcast: (msg: DataView) => server.send(new Uint8Array(msg.buffer), SEND_PORT, BCAST_IP), - }; -}; - -const notImpl = (sock: sock, dv: DataView) => { - const raw = dv.readU16(); - const cmd = CommandsByValue[raw]; - console.log(`^^ ${cmd} (${raw.toString(16)}) and it's not implemented yet`); -}; - -const noop = (sock: sock, dv: DataView) => {}; -const create_P2pAliveAck = (): DataView => { - const outbuf = new DataView(new Uint8Array(4).buffer); - outbuf.writeU16(Commands.P2PAliveAck); - outbuf.add(2).writeU16(0); - return outbuf; -}; - -const create_P2pRdy = (inbuf: DataView): DataView => { +export const create_P2pRdy = (inbuf: DataView): DataView => { const P2PRDY_SIZE = 0x14; const outbuf = new DataView(new Uint8Array(P2PRDY_SIZE + 4).buffer); outbuf.writeU16(Commands.P2pRdy); @@ -116,162 +93,3 @@ const create_P2pRdy = (inbuf: DataView): DataView => { outbuf.add(4).writeByteArray(new Uint8Array(inbuf.readByteArray(P2PRDY_SIZE).buffer)); return outbuf; }; - -let seen = [0]; -const handle_Drw = (sock: sock, dv: DataView) => { - // TODO - // INPUT - // byte 4 = d1 or d2, just add 1 - // byte 5 = stream?? - // byte 6-7 = pkt id - // OUTPUT - // f1d1 - // 2b len - // d1/d2 +1 (always d2?) - // 2b ack'd packets (1 for now, no coalescing) - // N times 2b with packet id - /* - * - 00000000 f1 d0 00 18 d1 00 00 00 11 0a 20 11 0c 00 ff 00 .......... ..... - 00000010 00 00 00 00 34 54 63 4d fe 01 01 01 ....4TcM.... - ^^^^^^^^^^^^^^ - some kind of challenge - need to send the 0x3010 command with this - but add 1 to every byte - */ - const should_be_d1 = dv.add(4).readU8(); - const m_stream = dv.add(5).readU8(); - const pkt_id = dv.add(6).readU16(); - const start_type = dv.add(8).readU16(); // 0xa11 - const cmd_id = dv.add(10).readU16(); // 0x1120 - console.log("DRW", should_be_d1, m_stream, pkt_id, start_type.toString(16), cmd_id.toString(16)); - - if (cmd_id == 0x2011) { - challenge[0] = dv.add(0x14).readU8(); - challenge[1] = dv.add(0x15).readU8(); - challenge[2] = dv.add(0x16).readU8(); - challenge[3] = dv.add(0x17).readU8(); - } - - const item_count = 1; // TODO - const reply_len = item_count * 2 + 4; // 4 hdr, 2b per item - const outbuf = new DataView(new Uint8Array(32).buffer); - outbuf.writeU16(Commands.DrwAck); - outbuf.add(2).writeU16(reply_len); - outbuf.add(4).writeU8(0xd2); - outbuf.add(5).writeU8(m_stream); - outbuf.add(6).writeU16(item_count); - for (let i = 0; i < item_count; i++) { - outbuf.add(8 + i * 2).writeU16(pkt_id); - } - sock.send(outbuf); - // CSession_Drw_Deal - // set some stuff? - // return a counter with pkt # ?? - // stream: 0 control - // 1 data?? video+aud - // Send_Pkt_DrwAck(10,0xd2,channel,1,&cmd_,sock_fd,ipaddr_); -}; -const challenge = [0, 0, 0, 0]; -const handle_P2PRdy = (sock: sock, dv: DataView) => { - const b = SendUsrChk("admin", "admin"); - sock.send(b); - setTimeout(() => { - /* - 00000000 f1 d0 01 14 d1 00 00 90 11 0a 10 30 08 01 00 00 ...........0.... - 00000010 77 78 35 69 01 01 01 01 wx5i.... - - 00000000 f1 d0 01 14 d1 00 00 04 11 0a 10 30 08 01 00 00 ...........0.... - 00000010 55 58 36 59 01 01 01 01 UX6Y.... - */ - let buf = new DataView(new Uint8Array(0x18).buffer); - const seq = 0x1; - let bytes = [ - 0xf1, - 0xd0, - 0x01, - 0x14, - 0xd1, - 0x00, - 0x00, - seq, - 0x11, - 0x0a, - 0x10, - 0x30, - 0x08, - 0x01, - 0x00, - 0x00, - challenge[0] % 2 == 0 ? challenge[0] + 1 : challenge[0] - 1, - challenge[1] % 2 == 0 ? challenge[1] + 1 : challenge[1] - 1, - challenge[2] % 2 == 0 ? challenge[2] + 1 : challenge[2] - 1, - challenge[3] % 2 == 0 ? challenge[3] + 1 : challenge[3] - 1, - 0x01, - 0x01, - 0x01, - 0x01, - ]; - buf.writeByteArray(bytes); - sock.send(buf); - }, 100); -}; -const handle_P2PAlive = (sock: sock, dv: DataView) => { - const b = create_P2pAliveAck(); - sock.send(b); -}; -const handle_PunchPkt = (sock: sock, dv: DataView) => { - console.log(`Got a nice punchpkt`); - const punchCmd = dv.readU16(); - const len = dv.add(2).readU16(); - const prefix = dv.add(4).readString(4); - const serial = dv.add(8).readU64().toString(); - const suffix = dv.add(16).readString(4); - // f141 20 BATC 609531 EXLV - console.log(punchCmd.toString(16), len, prefix, serial, suffix); - sock.send(create_P2pRdy(dv.add(4).readByteArray(len))); -}; - -export const Handlers: Record void> = { - // FIXME: keys are 'any' bc import? - PunchPkt: handle_PunchPkt, - - Close: notImpl, - LanSearchExt: notImpl, - LanSearch: notImpl, - P2PAlive: handle_P2PAlive, - P2PAliveAck: notImpl, - Hello: notImpl, - P2pRdy: handle_P2PRdy, - P2pReq: notImpl, - LstReq: notImpl, - DrwAck: noop, - Drw: handle_Drw, - - // From CSession_CtrlPkt_Proc, incomplete - PunchTo: notImpl, - HelloAck: notImpl, - RlyTo: notImpl, - DevLgnAck: notImpl, - P2PReqAck: notImpl, - ListenReqAck: notImpl, - RlyHelloAck: notImpl, // always - RlyHelloAck2: notImpl, // if len >1?? -}; - -const sock = MakeSock((msg, rinfo) => { - const ab = new Uint8Array(msg).buffer; - const dv = new DataView(ab); - const cmd = CommandsByValue[dv.readU16()]; - // ${rinfo.address}:${rinfo.port} - console.log(`<< ${cmd}`); - console.log(hexdump(ab, { useAnsi: true, ansiColor: 1 })); - Handlers[cmd](sock, dv); -}); - -const int = setInterval(() => { - let buf = new DataView(new Uint8Array(4).buffer); - create_LanSearch(buf); - sock.broadcast(buf); - // sock.send(buf); -}, 1000); diff --git a/proto_diag.md b/proto_diag.md index d203c53..2d712ba 100644 --- a/proto_diag.md +++ b/proto_diag.md @@ -24,7 +24,7 @@ title: Stream audio/video --- sequenceDiagram - App->>Cam: StreamStart (with Token, 0x3010) + App->>Cam: StreamStart (with Token) loop Cam-->>+App: Audio/Video Payload diff --git a/server.ts b/server.ts new file mode 100644 index 0000000..ab71c84 --- /dev/null +++ b/server.ts @@ -0,0 +1,102 @@ +import dgram from "node:dgram"; +import { create_LanSearch, Commands, CommandsByValue } from "./func_replacements.js"; +import { handle_P2PAlive, handle_PunchPkt, handle_P2PRdy, handle_Drw, notImpl, noop } from "./handlers.js"; +import { hexdump } from "./hexdump.js"; + +export type sock = { + send: (msg: DataView) => void; + broadcast: (msg: DataView) => void; +}; + +type opt = { + debug: boolean; + ansi: boolean; +}; + +type msgCb = (msg: Buffer, rinfo: any, options: opt) => void; +type connCb = () => void; + +const MakeSock = (cb: msgCb, connCb: connCb, options?: opt): sock => { + const server = dgram.createSocket("udp4"); + + server.on("error", (err) => { + console.error(`server error:\n${err.stack}`); + server.close(); + }); + + server.on("message", (msg, rinfo) => cb(msg, rinfo, options)); + + server.on("listening", () => { + const address = server.address(); + console.log(`server listening ${address.address}:${address.port}`); + server.setBroadcast(true); + connCb(); + }); + + const RECV_PORT = 49512; // important? + const DST_IP = "192.168.1.1"; + const BCAST_IP = "192.168.1.255"; + const SEND_PORT = 32108; + server.bind(RECV_PORT); + + return { + send: (msg: DataView) => { + const raw = msg.readU16(); + const cmd = CommandsByValue[raw]; + if (options.debug) { + console.log(`>> ${cmd}`); + console.log(hexdump(msg.buffer, { ansi: options.ansi, ansiColor: 0 })); + } + server.send(new Uint8Array(msg.buffer), SEND_PORT, DST_IP); + }, + broadcast: (msg: DataView) => server.send(new Uint8Array(msg.buffer), SEND_PORT, BCAST_IP), + }; +}; + +const Handlers: Record void> = { + // FIXME: keys are 'any' bc import? + PunchPkt: handle_PunchPkt, + + Close: notImpl, + LanSearchExt: notImpl, + LanSearch: notImpl, + P2PAlive: handle_P2PAlive, + P2PAliveAck: notImpl, + Hello: notImpl, + P2pRdy: handle_P2PRdy, + P2pReq: notImpl, + LstReq: notImpl, + DrwAck: noop, + Drw: handle_Drw, + + // From CSession_CtrlPkt_Proc, incomplete + PunchTo: notImpl, + HelloAck: notImpl, + RlyTo: notImpl, + DevLgnAck: notImpl, + P2PReqAck: notImpl, + ListenReqAck: notImpl, + RlyHelloAck: notImpl, // always + RlyHelloAck2: notImpl, // if len >1?? +}; + +const sock = MakeSock( + (msg, rinfo, options) => { + const ab = new Uint8Array(msg).buffer; + const dv = new DataView(ab); + const cmd = CommandsByValue[dv.readU16()]; + if (options.debug) { + console.log(`<< ${cmd}`); + console.log(hexdump(msg.buffer, { ansi: options.ansi, ansiColor: 1 })); + } + Handlers[cmd](sock, dv); + }, + () => { + const int = setInterval(() => { + let buf = new DataView(new Uint8Array(4).buffer); + create_LanSearch(buf); + sock.broadcast(buf); + }, 1000); + }, + { debug: false, ansi: false }, +); diff --git a/shim.ts b/shim.ts index 802b249..10f1059 100644 --- a/shim.ts +++ b/shim.ts @@ -47,12 +47,6 @@ DataView.prototype.readString = function (len) { return String.fromCharCode.apply(null, new Uint8Array(ba.buffer)); }; -const Memory = { - alloc: (len: number) => new DataView(new ArrayBuffer(len + 1)), - copy: (outbuf: DataView, inbuf: DataView, len: number) => - outbuf.writeByteArray(new Uint8Array(inbuf.readByteArray(len).buffer)), -}; - declare global { interface DataView { add(offset: number): DataView;