idz: Factor out common TCP obfuscation
This commit is contained in:
@@ -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<Buffer> {
|
||||||
|
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<void> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import setup from "./setup";
|
||||||
|
|
||||||
|
export { BLOCK_SIZE } from "./aes";
|
||||||
|
export default setup;
|
||||||
@@ -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 };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user