371 lines
12 KiB
Python
371 lines
12 KiB
Python
"""
|
|
AnyDesk Process Recon
|
|
Enumerates loaded modules, TLS-related exports, and interesting functions.
|
|
Run this first to understand AnyDesk's internals.
|
|
"""
|
|
import frida
|
|
import sys
|
|
import json
|
|
import time
|
|
|
|
AGENT_CODE = r"""
|
|
'use strict';
|
|
|
|
// ============================================================
|
|
// Phase 1: Enumerate all loaded modules
|
|
// ============================================================
|
|
function enumModules() {
|
|
const mods = Process.enumerateModules();
|
|
const result = [];
|
|
for (const m of mods) {
|
|
result.push({
|
|
name: m.name,
|
|
base: m.base.toString(),
|
|
size: m.size,
|
|
path: m.path
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// ============================================================
|
|
// Phase 2: Find TLS/crypto related exports across all modules
|
|
// ============================================================
|
|
function findTlsExports() {
|
|
const patterns = [
|
|
'SSL_read', 'SSL_write', 'SSL_new', 'SSL_connect',
|
|
'SSL_accept', 'SSL_free', 'SSL_CTX_new',
|
|
'EncryptMessage', 'DecryptMessage',
|
|
'SslEncryptPacket', 'SslDecryptPacket',
|
|
'BCryptEncrypt', 'BCryptDecrypt',
|
|
'CryptEncrypt', 'CryptDecrypt',
|
|
'mbedtls_ssl_read', 'mbedtls_ssl_write',
|
|
// Winsock
|
|
'send', 'recv', 'WSASend', 'WSARecv',
|
|
'sendto', 'recvfrom',
|
|
// File operations (for file transfer)
|
|
'CreateFileW', 'CreateFileA', 'WriteFile', 'ReadFile',
|
|
'MoveFileW', 'MoveFileExW', 'CopyFileW',
|
|
'CreateDirectoryW', 'RemoveDirectoryW',
|
|
// Memory alloc (for heap overflow detection)
|
|
'HeapAlloc', 'HeapFree', 'HeapReAlloc',
|
|
'VirtualAlloc', 'VirtualFree',
|
|
'malloc', 'free', 'realloc', 'calloc',
|
|
// Image/bitmap
|
|
'CreateDIBSection', 'SetDIBits', 'GetDIBits',
|
|
'CreateBitmap', 'CreateCompatibleBitmap',
|
|
// Clipboard
|
|
'SetClipboardData', 'GetClipboardData',
|
|
'OpenClipboard', 'CloseClipboard',
|
|
'EmptyClipboard',
|
|
];
|
|
|
|
const found = [];
|
|
const mods = Process.enumerateModules();
|
|
|
|
for (const m of mods) {
|
|
try {
|
|
const exports = m.enumerateExports();
|
|
for (const exp of exports) {
|
|
for (const pat of patterns) {
|
|
if (exp.name && exp.name.includes(pat)) {
|
|
found.push({
|
|
module: m.name,
|
|
name: exp.name,
|
|
address: exp.address.toString(),
|
|
type: exp.type
|
|
});
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Some modules can't be enumerated
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
|
|
// ============================================================
|
|
// Phase 3: Look for AnyDesk-specific strings in memory
|
|
// ============================================================
|
|
function findInterestingStrings() {
|
|
const mainModule = Process.enumerateModules()[0]; // AnyDesk.exe
|
|
const found = [];
|
|
const patterns = [
|
|
'DeskRT',
|
|
'file_transfer',
|
|
'FileTransfer',
|
|
'clipboard',
|
|
'Clipboard',
|
|
'identity',
|
|
'Identity',
|
|
'user_image',
|
|
'UserImage',
|
|
'avatar',
|
|
'codec',
|
|
'Codec',
|
|
'decompress',
|
|
'Decompress',
|
|
'decode_frame',
|
|
'DecodeFrame',
|
|
'bitmap',
|
|
'Bitmap',
|
|
'PNG',
|
|
'png_decode',
|
|
'traversal',
|
|
'path_sanitize',
|
|
'sanitize',
|
|
'validate_path',
|
|
'ad.security',
|
|
'ssl_ctx',
|
|
'TLS',
|
|
'handshake',
|
|
'Handshake',
|
|
];
|
|
|
|
for (const pat of patterns) {
|
|
try {
|
|
const matches = Memory.scanSync(mainModule.base, mainModule.size,
|
|
stringToPattern(pat));
|
|
if (matches.length > 0) {
|
|
found.push({
|
|
pattern: pat,
|
|
count: matches.length,
|
|
first_addr: matches[0].address.toString(),
|
|
// Read surrounding context
|
|
context: safeReadUtf8(matches[0].address.sub(16), 64)
|
|
});
|
|
}
|
|
} catch (e) {
|
|
// Scan failed for this pattern
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
|
|
function stringToPattern(str) {
|
|
let pat = '';
|
|
for (let i = 0; i < str.length; i++) {
|
|
if (i > 0) pat += ' ';
|
|
pat += str.charCodeAt(i).toString(16).padStart(2, '0');
|
|
}
|
|
return pat;
|
|
}
|
|
|
|
function safeReadUtf8(addr, len) {
|
|
try {
|
|
const buf = addr.readByteArray(len);
|
|
if (!buf) return '<null>';
|
|
const arr = new Uint8Array(buf);
|
|
let s = '';
|
|
for (let i = 0; i < arr.length; i++) {
|
|
const c = arr[i];
|
|
if (c >= 0x20 && c < 0x7f) s += String.fromCharCode(c);
|
|
else s += '.';
|
|
}
|
|
return s;
|
|
} catch (e) {
|
|
return '<unreadable>';
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Phase 4: Scan for OpenSSL-style function tables
|
|
// ============================================================
|
|
function findOpenSSLIndicators() {
|
|
const mods = Process.enumerateModules();
|
|
const indicators = [];
|
|
|
|
for (const m of mods) {
|
|
const nameLower = m.name.toLowerCase();
|
|
if (nameLower.includes('ssl') || nameLower.includes('crypto') ||
|
|
nameLower.includes('tls') || nameLower.includes('openssl') ||
|
|
nameLower.includes('mbedtls') || nameLower.includes('boringssl') ||
|
|
nameLower.includes('libcrypto') || nameLower.includes('libssl') ||
|
|
nameLower.includes('schannel') || nameLower.includes('ncrypt') ||
|
|
nameLower.includes('bcrypt') || nameLower.includes('sspicli')) {
|
|
indicators.push({
|
|
name: m.name,
|
|
base: m.base.toString(),
|
|
size: m.size,
|
|
path: m.path
|
|
});
|
|
}
|
|
}
|
|
return indicators;
|
|
}
|
|
|
|
// ============================================================
|
|
// Run all phases
|
|
// ============================================================
|
|
const report = {};
|
|
|
|
send({type: 'status', msg: 'Phase 1: Enumerating modules...'});
|
|
report.modules = enumModules();
|
|
send({type: 'status', msg: `Found ${report.modules.length} modules`});
|
|
|
|
send({type: 'status', msg: 'Phase 2: Finding TLS/crypto exports...'});
|
|
report.tls_exports = findTlsExports();
|
|
send({type: 'status', msg: `Found ${report.tls_exports.length} interesting exports`});
|
|
|
|
send({type: 'status', msg: 'Phase 3: Scanning for interesting strings...'});
|
|
report.strings = findInterestingStrings();
|
|
send({type: 'status', msg: `Found ${report.strings.length} string patterns`});
|
|
|
|
send({type: 'status', msg: 'Phase 4: Looking for TLS library indicators...'});
|
|
report.tls_indicators = findOpenSSLIndicators();
|
|
send({type: 'status', msg: `Found ${report.tls_indicators.length} TLS-related modules`});
|
|
|
|
send({type: 'result', data: report});
|
|
"""
|
|
|
|
def on_message(message, data):
|
|
if message['type'] == 'send':
|
|
payload = message['payload']
|
|
if payload.get('type') == 'status':
|
|
print(f" [*] {payload['msg']}")
|
|
elif payload.get('type') == 'result':
|
|
global result_data
|
|
result_data = payload['data']
|
|
elif message['type'] == 'error':
|
|
print(f" [!] ERROR: {message['description']}")
|
|
|
|
def find_anydesk_pid():
|
|
"""Find AnyDesk process ID"""
|
|
import subprocess
|
|
try:
|
|
output = subprocess.check_output(
|
|
['tasklist', '/FI', 'IMAGENAME eq AnyDesk.exe', '/FO', 'CSV', '/NH'],
|
|
text=True, stderr=subprocess.DEVNULL
|
|
)
|
|
for line in output.strip().split('\n'):
|
|
if 'AnyDesk' in line:
|
|
parts = line.strip().strip('"').split('","')
|
|
if len(parts) >= 2:
|
|
return int(parts[1].strip('"'))
|
|
except:
|
|
pass
|
|
return None
|
|
|
|
def main():
|
|
global result_data
|
|
result_data = None
|
|
|
|
print("=" * 60)
|
|
print(" AnyDesk Process Recon")
|
|
print("=" * 60)
|
|
|
|
pid = find_anydesk_pid()
|
|
if not pid:
|
|
print("[!] AnyDesk.exe not found. Make sure it's running.")
|
|
sys.exit(1)
|
|
|
|
print(f"[+] Found AnyDesk.exe (PID: {pid})")
|
|
print(f"[+] Attaching Frida...")
|
|
|
|
try:
|
|
session = frida.attach(pid)
|
|
except frida.ProcessNotFoundError:
|
|
print("[!] Could not attach to AnyDesk. Run as Administrator.")
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
print(f"[!] Frida attach failed: {e}")
|
|
print("[!] Make sure to run as Administrator.")
|
|
sys.exit(1)
|
|
|
|
print("[+] Injecting recon agent...")
|
|
script = session.create_script(AGENT_CODE, runtime='v8')
|
|
script.on('message', on_message)
|
|
script.load()
|
|
|
|
# Wait for results
|
|
timeout = 30
|
|
for i in range(timeout * 10):
|
|
if result_data is not None:
|
|
break
|
|
time.sleep(0.1)
|
|
|
|
if result_data is None:
|
|
print("[!] Timed out waiting for results")
|
|
session.detach()
|
|
sys.exit(1)
|
|
|
|
# Save and display results
|
|
output_path = "recon_results.json"
|
|
with open(output_path, 'w') as f:
|
|
json.dump(result_data, f, indent=2)
|
|
|
|
print(f"\n[+] Full results saved to {output_path}")
|
|
|
|
# Summary
|
|
print(f"\n{'=' * 60}")
|
|
print(" SUMMARY")
|
|
print(f"{'=' * 60}")
|
|
|
|
print(f"\n[Modules] {len(result_data['modules'])} loaded")
|
|
for m in result_data['modules'][:10]:
|
|
print(f" {m['name']:30s} @ {m['base']} ({m['size']:>10,} bytes)")
|
|
if len(result_data['modules']) > 10:
|
|
print(f" ... and {len(result_data['modules'])-10} more (see JSON)")
|
|
|
|
print(f"\n[TLS/Crypto Related Modules]")
|
|
if result_data['tls_indicators']:
|
|
for m in result_data['tls_indicators']:
|
|
print(f" {m['name']:30s} @ {m['base']} {m['path']}")
|
|
else:
|
|
print(" None found (AnyDesk may use statically linked TLS)")
|
|
|
|
print(f"\n[Interesting Exports] {len(result_data['tls_exports'])} found")
|
|
# Group by category
|
|
tls_funcs = [e for e in result_data['tls_exports']
|
|
if any(k in e['name'] for k in ['SSL_', 'Encrypt', 'Decrypt', 'mbedtls', 'Crypt'])]
|
|
net_funcs = [e for e in result_data['tls_exports']
|
|
if any(k in e['name'] for k in ['send', 'recv', 'WSA'])]
|
|
file_funcs = [e for e in result_data['tls_exports']
|
|
if any(k in e['name'] for k in ['CreateFile', 'WriteFile', 'ReadFile', 'MoveFile', 'CopyFile', 'Directory'])]
|
|
clip_funcs = [e for e in result_data['tls_exports']
|
|
if 'Clipboard' in e['name'] or 'clipboard' in e['name']]
|
|
img_funcs = [e for e in result_data['tls_exports']
|
|
if any(k in e['name'] for k in ['DIB', 'Bitmap', 'bitmap'])]
|
|
|
|
if tls_funcs:
|
|
print(f"\n TLS/Crypto ({len(tls_funcs)}):")
|
|
for e in tls_funcs[:15]:
|
|
print(f" {e['module']:20s} :: {e['name']:40s} @ {e['address']}")
|
|
|
|
if net_funcs:
|
|
print(f"\n Network ({len(net_funcs)}):")
|
|
for e in net_funcs[:10]:
|
|
print(f" {e['module']:20s} :: {e['name']:40s} @ {e['address']}")
|
|
|
|
if file_funcs:
|
|
print(f"\n File I/O ({len(file_funcs)}):")
|
|
for e in file_funcs[:10]:
|
|
print(f" {e['module']:20s} :: {e['name']:40s} @ {e['address']}")
|
|
|
|
if clip_funcs:
|
|
print(f"\n Clipboard ({len(clip_funcs)}):")
|
|
for e in clip_funcs:
|
|
print(f" {e['module']:20s} :: {e['name']:40s} @ {e['address']}")
|
|
|
|
if img_funcs:
|
|
print(f"\n Image/Bitmap ({len(img_funcs)}):")
|
|
for e in img_funcs:
|
|
print(f" {e['module']:20s} :: {e['name']:40s} @ {e['address']}")
|
|
|
|
print(f"\n[String Patterns in AnyDesk.exe]")
|
|
if result_data['strings']:
|
|
for s in result_data['strings']:
|
|
print(f" '{s['pattern']}' — {s['count']} hits, first @ {s['first_addr']}")
|
|
if s['context']:
|
|
print(f" context: {s['context']}")
|
|
else:
|
|
print(" No patterns found in main module")
|
|
|
|
session.detach()
|
|
print(f"\n[+] Done. Review {output_path} for full details.")
|
|
print("[+] Next step: run 02_sniffer.py to capture protocol traffic")
|
|
|
|
if __name__ == '__main__':
|
|
main()
|