'use strict'; const maxPrefixBytes = 96; const handles = new Map(); const sockets = new Map(); const pendingHooks = []; const installedHooks = new Set(); let getPeerName = null; function emit(kind, payload) { payload.kind = kind; payload.pid = Process.id; payload.tid = Process.getCurrentThreadId(); payload.timestamp = new Date().toISOString(); console.log('FRIDA_SYS ' + JSON.stringify(payload)); } function toHex(bytes) { const parts = []; for (let i = 0; i < bytes.length; i++) { const text = bytes[i].toString(16); parts.push(text.length === 1 ? '0' + text : text); } return parts.join(''); } function asciiPreview(bytes) { let text = ''; for (let i = 0; i < bytes.length; i++) { const b = bytes[i]; text += b >= 32 && b <= 126 ? String.fromCharCode(b) : '.'; } return text; } function payloadSummary(buffer, length, includePrefix) { if (includePrefix === undefined) { includePrefix = true; } const byteCount = Math.max(0, Math.min(length, maxPrefixBytes)); if (!includePrefix || buffer.isNull() || byteCount === 0) { return { byteCount: length, prefixByteCount: 0, prefixHex: '', asciiPrefix: '' }; } try { const raw = buffer.readByteArray(byteCount); const bytes = Array.from(new Uint8Array(raw)); return { byteCount: length, prefixByteCount: byteCount, prefixHex: toHex(bytes), asciiPrefix: asciiPreview(bytes) }; } catch (e) { return { byteCount: length, prefixByteCount: 0, prefixHex: '', asciiPrefix: '', error: String(e) }; } } function socketKey(socket) { return socket.toString(); } function readPortNetworkOrder(address) { const hi = address.readU8(); const lo = address.add(1).readU8(); return (hi << 8) | lo; } function parseSockaddr(address) { if (address.isNull()) { return null; } try { const family = address.readU16(); if (family === 2) { return { family: 'IPv4', address: [address.add(4).readU8(), address.add(5).readU8(), address.add(6).readU8(), address.add(7).readU8()].join('.'), port: readPortNetworkOrder(address.add(2)) }; } if (family === 23) { const parts = []; for (let i = 0; i < 16; i += 2) { parts.push(((address.add(8 + i).readU8() << 8) | address.add(8 + i + 1).readU8()).toString(16)); } return { family: 'IPv6', address: parts.join(':'), port: readPortNetworkOrder(address.add(2)) }; } return { family: String(family), address: null, port: null }; } catch (e) { return { error: String(e) }; } } function ensurePeer(socket) { const key = socketKey(socket); if (sockets.has(key) || getPeerName === null) { return; } try { const address = Memory.alloc(128); const length = Memory.alloc(4); length.writeU32(128); if (getPeerName(socket, address, length) === 0) { const peer = parseSockaddr(address); if (peer !== null) { sockets.set(key, peer); } } } catch (_) { } } function readWsaBuffers(lpBuffers, count) { const result = []; const stride = Process.pointerSize === 8 ? 16 : 8; const pointerOffset = Process.pointerSize === 8 ? 8 : 4; for (let i = 0; i < count; i++) { const item = lpBuffers.add(i * stride); const length = item.readU32(); const buffer = item.add(pointerOffset).readPointer(); result.push(payloadSummary(buffer, length)); } return result; } function readSecBufferDesc(secBufferDesc) { if (secBufferDesc.isNull()) { return null; } try { const version = secBufferDesc.readU32(); const bufferCount = secBufferDesc.add(4).readU32(); const buffersPointer = secBufferDesc.add(Process.pointerSize === 8 ? 8 : 8).readPointer(); const buffers = []; const stride = Process.pointerSize === 8 ? 16 : 12; const pointerOffset = Process.pointerSize === 8 ? 8 : 8; for (let i = 0; i < Math.min(bufferCount, 8); i++) { const item = buffersPointer.add(i * stride); const length = item.readU32(); const type = item.add(4).readU32(); const buffer = item.add(pointerOffset).readPointer(); buffers.push({ index: i, type, payload: payloadSummary(buffer, length) }); } return { version, bufferCount, buffers }; } catch (e) { return { error: String(e) }; } } function shouldCaptureFilePayload(path) { if (path === undefined || path === null) { return false; } return path.indexOf('\\pipe\\') !== -1 || path.indexOf('\\Device\\Afd') !== -1; } function hookExport(moduleName, exportName, callbacks) { const key = moduleName.toLowerCase() + '!' + exportName; if (installedHooks.has(key)) { return true; } try { let module = null; const targetModuleName = moduleName.toLowerCase(); for (const candidate of Process.enumerateModules()) { if (candidate.name.toLowerCase() === targetModuleName) { module = candidate; break; } } if (module === null) { module = Process.getModuleByName(moduleName); } const address = module.getExportByName(exportName); Interceptor.attach(address, callbacks); installedHooks.add(key); emit('hooked', { api: exportName, module: moduleName, address: address.toString() }); return true; } catch (e) { if (String(e).indexOf('unable to find module') !== -1) { if (pendingHooks.filter(h => h.key === key).length === 0) { pendingHooks.push({ key, moduleName, exportName, callbacks }); emit('hook-pending', { api: exportName, module: moduleName }); } return false; } if (String(e).indexOf('unable to find export') !== -1) { installedHooks.add(key); emit('hook-missing-export', { api: exportName, module: moduleName, error: String(e) }); return false; } emit('hook-error', { api: exportName, module: moduleName, error: String(e) }); return false; } } function installPendingHooks() { for (const hook of pendingHooks.slice()) { hookExport(hook.moduleName, hook.exportName, hook.callbacks); } } function readUnicodeString(unicodeString) { if (unicodeString.isNull()) { return ''; } try { const length = unicodeString.readU16(); const bufferOffset = Process.pointerSize === 8 ? 8 : 4; const buffer = unicodeString.add(bufferOffset).readPointer(); if (buffer.isNull() || length === 0) { return ''; } return buffer.readUtf16String(length / 2); } catch (e) { return ''; } } function readObjectAttributesName(objectAttributes) { if (objectAttributes.isNull()) { return ''; } try { const objectNameOffset = Process.pointerSize === 8 ? 16 : 8; const unicodeString = objectAttributes.add(objectNameOffset).readPointer(); return readUnicodeString(unicodeString); } catch (e) { return ''; } } function rememberHandle(handle, path, api) { if (!handle.equals(ptr('-1')) && !handle.isNull()) { handles.set(handle.toString(), path || ''); emit('handle-open', { api, handle: handle.toString(), path: path || '' }); } } function forgetHandle(handle, api, retval) { const key = handle.toString(); const path = handles.get(key); if (path !== undefined) { emit('handle-close', { api, handle: key, path, retval: retval.toString() }); handles.delete(key); } } emit('startup', { arch: Process.arch, platform: Process.platform, modules: Process.enumerateModules() .filter(m => { const lower = m.name.toLowerCase() + ' ' + m.path.toLowerCase(); return lower.indexOf('aah') !== -1 || lower.indexOf('historian') !== -1 || lower.indexOf('histsdk') !== -1; }) .map(m => ({ name: m.name, base: m.base.toString(), size: m.size, path: m.path })) }); try { Process.attachModuleObserver({ onAdded(module) { const lower = module.name.toLowerCase() + ' ' + module.path.toLowerCase(); if (lower.indexOf('aah') !== -1 || lower.indexOf('historian') !== -1 || lower.indexOf('histsdk') !== -1 || lower.indexOf('secur') !== -1 || lower.indexOf('netapi') !== -1) { emit('module-added', { name: module.name, base: module.base.toString(), size: module.size, path: module.path }); } installPendingHooks(); } }); } catch (e) { emit('module-observer-error', { error: String(e) }); } const pendingTimer = setInterval(() => { installPendingHooks(); if (pendingHooks.filter(h => !installedHooks.has(h.key)).length === 0) { clearInterval(pendingTimer); } }, 100); hookExport('kernel32.dll', 'CreateFileW', { onEnter(args) { this.path = args[0].isNull() ? '' : args[0].readUtf16String(); }, onLeave(retval) { rememberHandle(retval, this.path, 'CreateFileW'); } }); hookExport('kernel32.dll', 'CloseHandle', { onEnter(args) { this.handle = args[0]; }, onLeave(retval) { forgetHandle(this.handle, 'CloseHandle', retval); } }); hookExport('kernel32.dll', 'ReadFile', { onEnter(args) { this.handle = args[0]; this.path = handles.get(this.handle.toString()); this.buffer = args[1]; this.requested = args[2].toInt32(); this.readPointer = args[3]; }, onLeave(retval) { if (this.path === undefined) { return; } let read = null; try { read = this.readPointer.isNull() ? null : this.readPointer.readU32(); } catch (_) { read = null; } emit('read-file', { api: 'ReadFile', handle: this.handle.toString(), path: this.path, requestedBytes: this.requested, resultBytes: read, retval: retval.toInt32(), payload: payloadSummary(this.buffer, Math.max(0, read === null ? 0 : read), shouldCaptureFilePayload(this.path)) }); } }); hookExport('kernel32.dll', 'WriteFile', { onEnter(args) { this.handle = args[0]; this.path = handles.get(this.handle.toString()); this.buffer = args[1]; this.requested = args[2].toInt32(); this.writtenPointer = args[3]; }, onLeave(retval) { if (this.path === undefined) { return; } let written = null; try { written = this.writtenPointer.isNull() ? null : this.writtenPointer.readU32(); } catch (_) { written = null; } emit('write-file', { api: 'WriteFile', handle: this.handle.toString(), path: this.path, requestedBytes: this.requested, resultBytes: written, retval: retval.toInt32(), payload: payloadSummary(this.buffer, Math.max(0, written === null ? this.requested : written), shouldCaptureFilePayload(this.path)) }); } }); hookExport('ntdll.dll', 'NtCreateFile', { onEnter(args) { this.fileHandlePointer = args[0]; this.path = readObjectAttributesName(args[2]); }, onLeave(retval) { if (retval.toInt32() < 0) { return; } try { rememberHandle(this.fileHandlePointer.readPointer(), this.path, 'NtCreateFile'); } catch (e) { emit('handle-open-error', { api: 'NtCreateFile', path: this.path, error: String(e) }); } } }); hookExport('ntdll.dll', 'NtReadFile', { onEnter(args) { this.handle = args[0]; this.path = handles.get(this.handle.toString()); this.ioStatusBlock = args[4]; this.buffer = args[5]; this.requested = args[6].toInt32(); }, onLeave(retval) { if (this.path === undefined) { return; } let transferred = null; try { transferred = this.ioStatusBlock.add(Process.pointerSize).readPointer().toUInt32(); } catch (_) { transferred = retval.toInt32() >= 0 ? this.requested : 0; } emit('nt-read-file', { api: 'NtReadFile', handle: this.handle.toString(), path: this.path, requestedBytes: this.requested, resultBytes: transferred, ntstatus: retval.toString(), payload: payloadSummary(this.buffer, Math.max(0, transferred || 0), shouldCaptureFilePayload(this.path)) }); } }); hookExport('ntdll.dll', 'NtWriteFile', { onEnter(args) { this.handle = args[0]; this.path = handles.get(this.handle.toString()); this.ioStatusBlock = args[4]; this.buffer = args[5]; this.requested = args[6].toInt32(); }, onLeave(retval) { if (this.path === undefined) { return; } let transferred = null; try { transferred = this.ioStatusBlock.add(Process.pointerSize).readPointer().toUInt32(); } catch (_) { transferred = this.requested; } emit('nt-write-file', { api: 'NtWriteFile', handle: this.handle.toString(), path: this.path, requestedBytes: this.requested, resultBytes: transferred, ntstatus: retval.toString(), payload: payloadSummary(this.buffer, Math.max(0, transferred || this.requested), shouldCaptureFilePayload(this.path)) }); } }); hookExport('ntdll.dll', 'NtDeviceIoControlFile', { onEnter(args) { this.handle = args[0]; this.ioStatusBlock = args[4]; this.ioControlCode = args[5].toUInt32(); this.inputBuffer = args[6]; this.inputLength = args[7].toInt32(); this.outputBuffer = args[8]; this.outputLength = args[9].toInt32(); }, onLeave(retval) { let transferred = null; try { transferred = this.ioStatusBlock.add(Process.pointerSize).readPointer().toUInt32(); } catch (_) { transferred = null; } emit('nt-device-io-control', { api: 'NtDeviceIoControlFile', handle: this.handle.toString(), path: handles.get(this.handle.toString()), ioControlCode: '0x' + this.ioControlCode.toString(16), inputBytes: this.inputLength, outputBytes: this.outputLength, transferredBytes: transferred, ntstatus: retval.toString(), input: payloadSummary(this.inputBuffer, Math.max(0, this.inputLength), shouldCaptureFilePayload(handles.get(this.handle.toString()))), output: payloadSummary(this.outputBuffer, retval.toInt32() >= 0 ? Math.max(0, transferred === null ? this.outputLength : transferred) : 0, shouldCaptureFilePayload(handles.get(this.handle.toString()))) }); } }); hookExport('ws2_32.dll', 'GetAddrInfoW', { onEnter(args) { this.nodeName = args[0].isNull() ? '' : args[0].readUtf16String(); this.serviceName = args[1].isNull() ? '' : args[1].readUtf16String(); }, onLeave(retval) { emit('dns', { api: 'GetAddrInfoW', nodeName: this.nodeName, serviceName: this.serviceName, retval: retval.toInt32() }); } }); try { const ws2 = Process.getModuleByName('ws2_32.dll'); getPeerName = new NativeFunction(ws2.getExportByName('getpeername'), 'int', ['pointer', 'pointer', 'pointer']); } catch (e) { emit('getpeername-error', { error: String(e) }); } hookExport('ws2_32.dll', 'connect', { onEnter(args) { this.socket = args[0]; this.peer = parseSockaddr(args[1]); }, onLeave(retval) { if (this.peer !== null) { sockets.set(socketKey(this.socket), this.peer); } emit('socket-connect', { api: 'connect', socket: socketKey(this.socket), peer: this.peer, retval: retval.toInt32() }); } }); hookExport('ws2_32.dll', 'WSAConnect', { onEnter(args) { this.socket = args[0]; this.peer = parseSockaddr(args[1]); }, onLeave(retval) { if (this.peer !== null) { sockets.set(socketKey(this.socket), this.peer); } emit('socket-connect', { api: 'WSAConnect', socket: socketKey(this.socket), peer: this.peer, retval: retval.toInt32() }); } }); hookExport('ws2_32.dll', 'send', { onEnter(args) { this.socket = args[0]; this.buffer = args[1]; this.length = args[2].toInt32(); }, onLeave(retval) { ensurePeer(this.socket); emit('socket-send', { api: 'send', socket: socketKey(this.socket), peer: sockets.get(socketKey(this.socket)), requestedBytes: this.length, resultBytes: retval.toInt32(), payload: payloadSummary(this.buffer, Math.max(0, Math.min(this.length, retval.toInt32()))) }); } }); hookExport('ws2_32.dll', 'recv', { onEnter(args) { this.socket = args[0]; this.buffer = args[1]; }, onLeave(retval) { const length = retval.toInt32(); if (length <= 0) { return; } ensurePeer(this.socket); emit('socket-recv', { api: 'recv', socket: socketKey(this.socket), peer: sockets.get(socketKey(this.socket)), resultBytes: length, payload: payloadSummary(this.buffer, length) }); } }); hookExport('ws2_32.dll', 'WSASend', { onEnter(args) { this.socket = args[0]; this.buffers = args[1]; this.count = args[2].toInt32(); this.sentPointer = args[3]; this.summaries = readWsaBuffers(this.buffers, this.count); }, onLeave(retval) { let sent = null; try { sent = this.sentPointer.isNull() ? null : this.sentPointer.readU32(); } catch (_) { sent = null; } ensurePeer(this.socket); emit('socket-send', { api: 'WSASend', socket: socketKey(this.socket), peer: sockets.get(socketKey(this.socket)), retval: retval.toInt32(), resultBytes: sent, buffers: this.summaries }); } }); hookExport('ws2_32.dll', 'WSARecv', { onEnter(args) { this.socket = args[0]; this.buffers = args[1]; this.count = args[2].toInt32(); this.receivedPointer = args[3]; }, onLeave(retval) { let received = null; try { received = this.receivedPointer.isNull() ? null : this.receivedPointer.readU32(); } catch (_) { received = null; } ensurePeer(this.socket); emit('socket-recv', { api: 'WSARecv', socket: socketKey(this.socket), peer: sockets.get(socketKey(this.socket)), retval: retval.toInt32(), resultBytes: received, buffers: readWsaBuffers(this.buffers, this.count) }); } }); hookExport('ws2_32.dll', 'WSAIoctl', { onEnter(args) { this.socket = args[0]; this.controlCode = args[1].toUInt32(); this.inputBuffer = args[2]; this.inputLength = args[3].toInt32(); this.outputBuffer = args[4]; this.outputLength = args[5].toInt32(); this.bytesReturnedPointer = args[6]; }, onLeave(retval) { let bytesReturned = null; try { bytesReturned = this.bytesReturnedPointer.isNull() ? null : this.bytesReturnedPointer.readU32(); } catch (_) { bytesReturned = null; } ensurePeer(this.socket); emit('socket-ioctl', { api: 'WSAIoctl', socket: socketKey(this.socket), peer: sockets.get(socketKey(this.socket)), controlCode: '0x' + this.controlCode.toString(16), inputBytes: this.inputLength, outputBytes: this.outputLength, bytesReturned, retval: retval.toInt32(), input: payloadSummary(this.inputBuffer, Math.max(0, this.inputLength)), output: payloadSummary(this.outputBuffer, Math.max(0, bytesReturned === null ? 0 : bytesReturned)) }); } }); hookExport('mswsock.dll', 'ConnectEx', { onEnter(args) { this.socket = args[0]; this.peer = parseSockaddr(args[1]); this.sendBuffer = args[3]; this.sendBytes = args[4].toInt32(); }, onLeave(retval) { if (this.peer !== null) { sockets.set(socketKey(this.socket), this.peer); } emit('socket-connect', { api: 'ConnectEx', socket: socketKey(this.socket), peer: this.peer, retval: retval.toInt32(), initialPayload: payloadSummary(this.sendBuffer, Math.max(0, this.sendBytes)) }); } }); hookExport('mswsock.dll', 'TransmitPackets', { onEnter(args) { this.socket = args[0]; this.packetArray = args[1]; this.elementCount = args[2].toInt32(); this.sendSize = args[3].toInt32(); }, onLeave(retval) { ensurePeer(this.socket); emit('socket-send', { api: 'TransmitPackets', socket: socketKey(this.socket), peer: sockets.get(socketKey(this.socket)), elementCount: this.elementCount, sendSize: this.sendSize, retval: retval.toInt32() }); } }); hookExport('secur32.dll', 'AcquireCredentialsHandleW', { onEnter(args) { this.principal = args[0].isNull() ? '' : args[0].readUtf16String(); this.packageName = args[1].isNull() ? '' : args[1].readUtf16String(); this.credentialUse = args[2].toUInt32(); }, onLeave(retval) { emit('secur32', { api: 'AcquireCredentialsHandleW', packageName: this.packageName, principalPresent: this.principal.length > 0, credentialUse: this.credentialUse, status: retval.toString() }); } }); hookExport('sspicli.dll', 'AcquireCredentialsHandleW', { onEnter(args) { this.principal = args[0].isNull() ? '' : args[0].readUtf16String(); this.packageName = args[1].isNull() ? '' : args[1].readUtf16String(); this.credentialUse = args[2].toUInt32(); }, onLeave(retval) { emit('secur32', { api: 'AcquireCredentialsHandleW', module: 'sspicli.dll', packageName: this.packageName, principalPresent: this.principal.length > 0, credentialUse: this.credentialUse, status: retval.toString() }); } }); hookExport('secur32.dll', 'InitializeSecurityContextW', { onEnter(args) { this.target = args[2].isNull() ? '' : args[2].readUtf16String(); this.requestFlags = args[3].toUInt32(); this.inputToken = readSecBufferDesc(args[6]); this.outputTokenPointer = args[9]; }, onLeave(retval) { emit('secur32', { api: 'InitializeSecurityContextW', target: this.target, requestFlags: this.requestFlags, inputToken: this.inputToken, outputToken: readSecBufferDesc(this.outputTokenPointer), status: retval.toString() }); } }); hookExport('sspicli.dll', 'InitializeSecurityContextW', { onEnter(args) { this.target = args[2].isNull() ? '' : args[2].readUtf16String(); this.requestFlags = args[3].toUInt32(); this.inputToken = readSecBufferDesc(args[6]); this.outputTokenPointer = args[9]; }, onLeave(retval) { emit('secur32', { api: 'InitializeSecurityContextW', module: 'sspicli.dll', target: this.target, requestFlags: this.requestFlags, inputToken: this.inputToken, outputToken: readSecBufferDesc(this.outputTokenPointer), status: retval.toString() }); } }); hookExport('secur32.dll', 'AcceptSecurityContext', { onEnter(args) { this.requestFlags = args[3].toUInt32(); }, onLeave(retval) { emit('secur32', { api: 'AcceptSecurityContext', requestFlags: this.requestFlags, status: retval.toString() }); } }); hookExport('sspicli.dll', 'AcceptSecurityContext', { onEnter(args) { this.requestFlags = args[3].toUInt32(); }, onLeave(retval) { emit('secur32', { api: 'AcceptSecurityContext', module: 'sspicli.dll', requestFlags: this.requestFlags, status: retval.toString() }); } }); hookExport('secur32.dll', 'QuerySecurityContextToken', { onLeave(retval) { emit('secur32', { api: 'QuerySecurityContextToken', status: retval.toString() }); } }); hookExport('sspicli.dll', 'QuerySecurityContextToken', { onLeave(retval) { emit('secur32', { api: 'QuerySecurityContextToken', module: 'sspicli.dll', status: retval.toString() }); } }); hookExport('crypt32.dll', 'CryptBinaryToStringW', { onEnter(args) { this.inputBytes = args[1].toUInt32(); this.flags = args[2].toUInt32(); this.outputLengthPointer = args[4]; }, onLeave(retval) { let outputChars = null; try { outputChars = this.outputLengthPointer.isNull() ? null : this.outputLengthPointer.readU32(); } catch (_) { outputChars = null; } emit('crypt32', { api: 'CryptBinaryToStringW', inputBytes: this.inputBytes, flags: this.flags, outputChars, retval: retval.toInt32() }); } }); hookExport('crypt32.dll', 'CryptStringToBinaryW', { onEnter(args) { this.inputChars = args[1].toUInt32(); this.flags = args[2].toUInt32(); this.outputLengthPointer = args[4]; }, onLeave(retval) { let outputBytes = null; try { outputBytes = this.outputLengthPointer.isNull() ? null : this.outputLengthPointer.readU32(); } catch (_) { outputBytes = null; } emit('crypt32', { api: 'CryptStringToBinaryW', inputChars: this.inputChars, flags: this.flags, outputBytes, retval: retval.toInt32() }); } }); hookExport('netapi32.dll', 'NetUserGetLocalGroups', { onEnter(args) { this.server = args[0].isNull() ? '' : args[0].readUtf16String(); this.user = args[1].isNull() ? '' : args[1].readUtf16String(); }, onLeave(retval) { emit('netapi', { api: 'NetUserGetLocalGroups', server: this.server, user: this.user, status: retval.toString() }); } });