From 2dfeeaa8bd3e13d9bb8a398b8fb63f7bca147335 Mon Sep 17 00:00:00 2001 From: Tau Date: Wed, 11 Mar 2020 17:27:50 -0400 Subject: [PATCH] idz: Factor out common TCP obfuscation --- src/idz/common/aes.ts | 43 ++++++++++++++++++++++++ src/idz/common/bigint.ts | 46 ++++++++++++++++++++++++++ src/idz/common/index.ts | 4 +++ src/idz/common/setup.ts | 70 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 163 insertions(+) create mode 100644 src/idz/common/aes.ts create mode 100644 src/idz/common/bigint.ts create mode 100644 src/idz/common/index.ts create mode 100644 src/idz/common/setup.ts diff --git a/src/idz/common/aes.ts b/src/idz/common/aes.ts new file mode 100644 index 0000000..ede6f59 --- /dev/null +++ b/src/idz/common/aes.ts @@ -0,0 +1,43 @@ +import { Decipher, Cipher, createCipheriv, createDecipheriv } from "crypto"; + +import { ByteStream } from "../../util/stream"; + +export const BLOCK_SIZE = 16; + +export default class AesEcbStream implements ByteStream { + private readonly _dec: Decipher; + private readonly _enc: Cipher; + + constructor(private _stm: ByteStream, keybuf: Buffer) { + this._dec = createDecipheriv("aes-128-ecb", keybuf, null).setAutoPadding( + false + ); + this._enc = createCipheriv("aes-128-ecb", keybuf, null).setAutoPadding( + false + ); + } + + close() { + this._stm.close(); + } + + async read(nbytes: number): Promise { + if (nbytes % BLOCK_SIZE !== 0) { + throw new Error("Attempted to read partial cipher block"); + } + + const ciphertext = await this._stm.read(nbytes); + + return this._dec.update(ciphertext); + } + + async write(buf: Buffer): Promise { + if (buf.length % BLOCK_SIZE !== 0) { + throw new Error("Attempted to write partial cipher block"); + } + + const ciphertext = this._enc.update(buf); + + await this._stm.write(ciphertext); + } +} diff --git a/src/idz/common/bigint.ts b/src/idz/common/bigint.ts new file mode 100644 index 0000000..6fbee8d --- /dev/null +++ b/src/idz/common/bigint.ts @@ -0,0 +1,46 @@ +export function readBigInt(buf: Buffer) { + let result = 0n; + + for (let i = 0; i < buf.length; i++) { + const shift = 8n * BigInt(i); + const byte = buf.readUInt8(i); + + result |= BigInt(byte) << shift; + } + + return result; +} + +export function writeBigInt(n: bigint, length: number) { + const result = Buffer.alloc(length); + + for (let i = 0; i < length; i++) { + const shift = 8n * BigInt(i); + const byte = (n >> shift) & 0xffn; + + result[i] = Number(byte); + } + + return result; +} + +// i pick the one implementation language that doesn't have this built in + +export function modPow(b: bigint, e: bigint, m: bigint) { + // https://en.wikipedia.org/wiki/Modular_exponentiation#Right-to-left_binary_method + + let result = 1n; + + b = b % m; + + while (e > 0n) { + if ((e & 1n) === 1n) { + result = (result * b) % m; + } + + e = e >> 1n; + b = (b * b) % m; + } + + return result; +} diff --git a/src/idz/common/index.ts b/src/idz/common/index.ts new file mode 100644 index 0000000..35efa22 --- /dev/null +++ b/src/idz/common/index.ts @@ -0,0 +1,4 @@ +import setup from "./setup"; + +export { BLOCK_SIZE } from "./aes"; +export default setup; diff --git a/src/idz/common/setup.ts b/src/idz/common/setup.ts new file mode 100644 index 0000000..1bff9dd --- /dev/null +++ b/src/idz/common/setup.ts @@ -0,0 +1,70 @@ +import { Socket } from "net"; + +import AesEcbStream from "./aes"; +import { readBigInt, modPow, writeBigInt } from "./bigint"; +import IoByteStream from "../../util/stream"; + +interface RsaKey { + N: bigint; + d: bigint; + e: bigint; + hashN: number; +} + +// Proof-of-concept, so we only ever use one of the ten RSA key pairs +const rsaKey = { + N: 4922323266120814292574970172377860734034664704992758249880018618131907367614177800329506877981986877921220485681998287752778495334541127048495486311792061n, + d: 1163847742215766215216916151663017691387519688859977157498780867776436010396072628219119707788340687440419444081289736279466637153082223960965411473296473n, + e: 3961365081960959178294197133768419551060435043430437330799371731939550352626564261219865471710058480523874787120718634318364066605378505537556570049131337n, + hashN: 2662304617, +}; + +// Proof-of-concept, so we only use one fixed session key +const aesKey = Buffer.from("ffddeeccbbaa99887766554433221100", "hex"); + +function writeServerHello(aesKey: Buffer, rsaKey: RsaKey) { + const M = readBigInt(aesKey); + const keyEnc = modPow(M, rsaKey.e, rsaKey.N); + const result = Buffer.alloc(0x48); + + result.set(writeBigInt(keyEnc, 0x40), 0x00); + result.writeUInt32LE(0x01020304, 0x40); // Meaning is unknown + result.writeUInt32LE(rsaKey.hashN, 0x44); + + return result; +} + +function readClientHello(buf: Buffer) { + const magic = buf.readUInt32LE(0x00); + + if (magic !== 0x01020304) { + throw new Error( + "Invalid magic number, cryptographic processing probably incorrect." + ); + } + + return { + pcbId: buf.slice(0x04, 0x0f).toString("ascii"), + protocol: buf.slice(0x10, 0x13).toString("ascii"), + model: buf.slice(0x18, 0x0c).toString("ascii"), + }; +} + +export default async function setup(socket: Socket) { + const tcpStream = new IoByteStream(socket); + const serverHello = writeServerHello(aesKey, rsaKey); + + await tcpStream.write(serverHello); + + const aesStream = new AesEcbStream(tcpStream, aesKey); + + const response = await aesStream.read(0x30); + + if (response.length !== 0x30) { + throw new Error("Truncated client hello"); + } + + const clientHello = readClientHello(response); + + return { aesStream, clientHello }; +}