+2

Tấn Công Embedding Trong Hệ Thống RAG - Phần 2: "Trinh Sát Và Chọn Mục Tiêu - Từ Vector DB Đến Mục Tiêu Giá Trị Cao"

Mở đầu: Bạn đã đột nhập được, giờ thì sao?

Hãy tiếp tục câu chuyện của chúng ta từ Phần 1.

Bạn đã vào được tòa nhà. Bạn đứng trước căn phòng thứ hai - cái phòng với cửa kính cường lực mà mọi người tưởng là an toàn vì nó chỉ lưu "dấu ấn ngữ nghĩa". Bạn mỉm cười. Bạn đã biết rằng embedding không phải hàm băm, và bây giờ bạn muốn chứng minh điều đó.

Nhưng đợi đã. Căn phòng này không chỉ có một cái két. Nó chứa một thư viện khổng lồ với hàng chục nghìn tài liệu - tất cả đều được lưu dưới dạng embedding. Giống như một thư viện không có tên sách, chỉ có "dấu vân tay ngữ nghĩa" của từng cuốn.

Câu hỏi đặt ra: Làm sao tìm ra cuốn sách quý giá nhất - cuốn có chứa mật khẩu admin, khóa API AWS, hay thông tin tài chính nội bộ - trong đống hỗn độn này?

Đây là lúc trinh sátchọn mục tiêu phát huy tác dụng. Phần này sẽ không dạy bạn cách mở két - việc đó để dành cho Phần 3 và 4. Phần này dạy bạn cách tìm đúng cái két để mở.

Hãy cùng tôi thực hiện một nhiệm vụ giả định. Chúng ta vừa SSH thành công vào một máy chủ nội bộ. Trên máy chủ đó, một cổng 8080 đang mở. Đó là Weaviate - một cơ sở dữ liệu vector phổ biến. Và điều khiến trái tim tôi đập nhanh hơn: nó không yêu cầu xác thực.


1. Bước 0: Xuất khẩu Embeddings - Lấy "dấu vân tay" về nhà

Trước khi làm bất cứ điều gì, hãy lấy toàn bộ embeddings ra khỏi hệ thống. Vì sao? Vì một khi bạn đã có embeddings trong tay, bạn có thể phân tích offline, không bị phát hiện, không lo bị chặn, và quan trọng nhất - bạn có thời gian để thử sai.

1.1. Kiểm tra xem có thể kết nối đến mục tiêu và xem có những collection nào.

  • Tạo file enum1.py
import requests

WEAVIATE_URL = "http://localhost:8080"

response = requests.get(f"{WEAVIATE_URL}/v1/schema")
response.raise_for_status()

schema = response.json()

# Extract all collection (class) names
collections = [c["class"] for c in schema.get("classes", [])]

print("Collections:", collections)
  • Chạy script
attacker@rag:~$ python3 enum1.py

Kết quả mong đợi: Collections: ['DocChunk']

Noted: Script này gửi một yêu cầu HTTP GET đến endpoint /v1/schema của Weaviate để lấy thông tin về tất cả các collection (trong Weaviate gọi là "classes"). Kết quả cho thấy có một collection tên là DocChunk - đây là nơi lưu trữ các đoạn văn bản và embeddings của chúng.

1.2. Xuất khẩu tất cả các Embeddings

Bây giờ chúng ta sẽ xuất khẩu tất cả embeddings từ collection DocChunk sang các file NumPy, CSV và Parquet. Viết script (link) để export toàn bộ Embeddings của DocChunk Collection

# Kết nối đến Weaviate
client = weaviate.Client("http://localhost:8080")
# Lấy tất cả vector embeddings
results = client.query.get("DocChunk", ["text", "_additional { vector }"]).do()
# Lưu vào file .npy
np.save("embeddings.npy", vectors)

Output mong đợi

Connected to Weaviate via raw HTTP. Collection: DocChunk
Total objects: 1
Exporting embeddings: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 192.80it/s]
Vectors successfully exported!
Exported 31 vectors to ./export/

Và thế là xong. Bạn có một file embeddings.npy - một mảng NumPy chứa các vector số. Nếu ai đó hỏi, bạn chỉ có một file dữ liệu khoa học. Nhưng với chúng ta, đó là kho báu.

Note: Script này sử dụng GraphQL cursor-based pagination để lấy tất cả embeddings từ Weaviate mà không bị giới hạn số lượng. Nó lưu:

  • embeddings.npy: Mảng NumPy chứa tất cả vector embeddings (float32)

  • chunk_ids.npy: ID của từng chunk

  • uuids.npy: UUID của từng object trong Weaviate

  • embeddings.csv: File CSV dễ đọc

  • embeddings.parquet: File Parquet tiết kiệm dung lượng


2. Bước 1: Xác định mô hình Embedding - "Ai đã tạo ra thứ này?"

Đây là bước quan trọng nhất và cũng là bước dễ bị xem nhẹ nhất. Để đảo ngược embedding, bạn phải biết mô hình nào đã tạo ra chúng.

Hãy tưởng tượng bạn tìm thấy một mã khóa. Bạn không thể mở được nếu không biết nhà sản xuất, dòng sản phẩm, và năm sản xuất. Embedding cũng vậy. Mỗi mô hình embedding - all-MiniLM-L6-v2, text-embedding-ada-002, bge-large, v.v. - có không gian vector riêng của nó.

Hai mô hình khác nhau, cùng một câu văn, sẽ cho ra hai vector khác nhau hoàn toàn. Để đảo ngược, bạn phải biết bạn đang ở trong "không gian" nào.

Vậy làm sao để biết? Có vài cách, từ đơn giản đến tinh vi:

2.1. 🔍 Cách 1: Nhìn vào kích thước (Dimension Fingerprinting)

Mỗi mô hình embedding có một kích thước vector đặc trưng:

Mô hình Kích thước
all-MiniLM-L6-v2 384
bge-small-en 384
text-embedding-ada-002 1536
Cohere-embed-multilingual-v3 1024
BAAI/bge-large-en-v1.5 1024
  • Tạo file check_dim.py:
import numpy as np

# Load embeddings
embeddings = np.load('export/embeddings.npy')
print(f"Shape: {embeddings.shape}")
print(f"Dimension: {embeddings.shape[1]}")
print(f"Number of chunks: {embeddings.shape[0]}")

Chạy script check_dim.py, kết quả mong đợi:

Shape: (31, 384)
Dimension: 384
Number of chunks: 31

Điều này giúp ta loại bỏ ngay hàng chục test cases. Nhưng vẫn còn nhiều mô hình 384 chiều (all-MiniLM-L6-v2, multi-qa-MiniLM-L6-cos-v1, hoặc paraphrase-MiniLM-L6-v2) nên ta cần thêm thông tin.

2.2. 🧠 Cách 2: Thăm dò Suy luận (Inference Probing) - Chiến thuật tinh vi nhất

Đây là cách tinh vi nhất để xác định mô hình embedding bằng cách truy vấn RAG và so sánh kết quả.

Hãy tưởng tượng hệ thống RAG có một giao diện chat - bạn hỏi một câu, nó tìm kiếm embeddings tương tự, lấy ra các đoạn văn liên quan, và trả lời. Bây giờ, thay vì hỏi những câu vô hại, chúng ta hỏi những câu được thiết kế đặc biệt để tiết lộ mô hình embedding.

Cách hoạt động:

  1. Gửi câu hỏi đến RAG (ví dụ: "Hãy trích dẫn chính xác nội dung của tài liệu X").
  2. Nhận về phản hồi - có thể là văn bản đã được tóm tắt.
  3. Chia phản hồi thành các câu hoặc cặp câu.
  4. Nhúng các câu đó bằng từng ứng viên mô hình có kích thước 384.
  5. Tính độ tương tự cosine với các embeddings đã xuất khẩu.
  6. Ứng viên nào cho độ tương tự cao nhất (>0.85) - đó chính là mô hình đúng.

Thực tế, tôi đã chạy một script tự động hóa quá trình này vào file inference_probing.py.

Mô hình đúng sẽ tạo ra các vector nằm gần với các vector được lưu trữ, thường là 0,85 hoặc cao hơn đối với các đoạn diễn giải gần đúng các đoạn thực tế, trong khi các mô hình sai đạt điểm khoảng 0,5–0,6 ngay cả đối với cùng một văn bản, vì không gian embedding của chúng có hình học khác nhau mặc dù có cùng kích thước. Tập lệnh xếp hạng các ứng viên theo độ tương tự trung bình của 5 ứng viên hàng đầu, tập trung vào các đoạn khớp tốt nhất và bỏ qua văn bản đệm của LLM vốn khớp kém bất kể mô hình. Nó báo cáo mô hình được xác định cùng với mức độ tin cậy dựa trên khoảng cách so với ứng viên tiếp theo và tùy chọn lưu các cặp văn bản-embedding đã khớp để sử dụng trực tiếp làm dữ liệu căn chỉnh ALGEN (Alignment Generation hoặc Aligned Generation).

  • Viết script (link) inference_probing.py
  • Thực hiện chạy script:
attacker@rag:~$ python3 inference_probing.py export/embeddings.npy --url http://127.0.0.1:80

image.png

Output chỉ ra all-MiniLM-L6-v2 được xác định với độ tin cậy CAO

Tại sao nó hoạt động? Vì khi bạn nhúng một câu bằng đúng mô hình, vector của nó sẽ "nằm sát" với vector gốc được lưu trong database. Như hai dấu vân tay trùng khớp. Sai mô hình, cho dù cùng kích thước, vẫn cho vector "bay xa" khỏi mục tiêu.

Thông tin thêm: Nếu bạn không có quyền truy cập vào giao diện chat RAG? Bạn vẫn có thể thử:

  • Tìm file cấu hình trên hệ thống
  • Tra tài liệu dự án trên GitHub nội bộ
  • Thậm chí... gọi điện cho bộ phận IT giả vờ là nhà cung cấp để hỏi thông tin kỹ thuật (nhưng chỉ làm điều này khi được phép trong phạm vi nhiệm vụ nhé!).

3. Bước 2: Phân loại đoạn (Chunk Triaging) - Tìm kim trong đống cỏ

Giờ bạn đã có 10.000 embeddings và biết mô hình đã tạo ra chúng. Nhưng bạn có 2 ngày để hoàn thành nhiệm vụ. Đảo ngược mỗi embedding mất vài phút. Bạn không thể xử lý tất cả 31 chunk.

Bạn cần một bộ lọc thông minh.

Phân loại đoạn là kỹ thuật giúp bạn tìm ra những embeddings chứa thông tin nhạy cảm mà không cần giải mã chúng. Nó hoạt động dựa trên một ý tưởng đơn giản nhưng cực kỳ hiệu quả:

"Một embedding chứa mật khẩu sẽ giống với embedding của các văn bản khác có chủ đề về mật khẩu."

Hay nói cách khác: Bạn không cần biết mật khẩu là gì để biết đoạn văn này đang nói về mật khẩu. Bạn chỉ cần so sánh embedding của nó với các "mẫu dò" (probe templates) mà bạn đã chuẩn bị trước.

🎯 Thực hành: Pipeline phân loại 3 tầng

  • Tạo file chunk_triage_pipe.py (link):
  • Thực hiện chạy script vừa tạo:
attacker@rag:~$ python3 chunk_triage_pipe.py export/embeddings.npy
  • Kết quả mong đợi:

image.png

Giải thích: Pipeline phân loại 3 tầng:

    • Tầng 1 - Mật độ: Xác định các chunk "cô lập" trong không gian vector - những chunk ít có hàng xóm ngữ nghĩa, thường là dấu hiệu của thông tin đặc biệt
    • Tầng 2 - Pairwise: So sánh với mẫu dò tích cực (chứa thông tin nhạy cảm) và tiêu cực (nội dung lành tính). Chunk #23 đạt điểm cao thứ hai ở tầng này
    • Tầng 3 - Tái tạo: Thực hiện đảo ngược nông để xác định cấu trúc của chunk
    • Fusion: Kết hợp điểm số từ 3 tầng bằng Reciprocal Rank Fusion với trọng số tăng dần

Kết quả: Top 3 chunk có điểm số tập trung ở 0.071+, trong khi chunk thứ 4 giảm xuống 0.068 - một khoảng cách rõ rệt. Đây là ngưỡng cắt tự nhiên, giúp chúng ta tập trung vào chỉ 3 mục tiêu thay vì 31.


4. Trích xuất mục tiêu cụ thể

Sau khi phân loại, chúng ta sẽ tập trung vào Chunk #23 - chunk xếp hạng thứ 3.

  • Thực hiện trích xuất Chunk #23
attacker@rag:~$ python3 -c "import numpy as np; np.save('chunk_23.npy', np.load('export/embeddings.npy')[23:24])"

Kết quả: File chunk_23.npy được tạo, chứa embedding của chunk thứ 23.


5. Một thực tế cần nhớ: Đừng chủ quan với "đoạn ngắn"

Trong nhiệm vụ của chúng ta, các đoạn văn khá ngắn - khoảng 20-30 token. Đó là tin tốt cho chúng ta, vì các công cụ đảo ngược embedding hiện tại hoạt động tốt nhất với đoạn ngắn (32-64 token).

Nhưng trong thực tế, một số hệ thống RAG sử dụng đoạn 256-512 token. Khi đó, việc phân loại vẫn hiệu quả, nhưng đảo ngược toàn bộ nội dung trở nên khó khăn. Tuy nhiên, các đoạn chứa thông tin xác thực thường không dài - chúng chỉ là 1-2 câu. Và đó là lúc các kỹ thuật của chúng ta phát huy tác dụng tối đa.


6. Tóm tắt: 3 bài học từ phần này

  1. Xuất khẩu embeddings là bước đầu tiên và dễ nhất. Chỉ cần một script Python và quyền đọc database. Làm điều này trước khi bị phát hiện. Lưu ở cả ba định dạng: NumPy (chính xác nhất), Parquet (cân bằng), và CSV (dễ đọc).

  2. Xác định mô hình embedding là chìa khóa. Đừng đoán mò. Dùng kích thước làm chỉ dẫn, và "thăm dò suy luận" qua RAG để xác định chính xác. Đúng mô hình = 80% chiến thắng.

  3. Phân loại đoạn giúp bạn tập trung vào mục tiêu giá trị cao. Trong một đại dương embeddings, các kỹ thuật mật độ, mẫu dò, và tái tạo nông giúp bạn thu hẹp từ hàng nghìn xuống vài ba mục tiêu - tiết kiệm thời gian và công sức.


7. Preview Phần 3

Trong phần tiếp theo - "Đảo Ngược Zero-Shot - Khi Mẫu Dò Và Tìm Kiếm Chùm Lên Tiếng" - chúng ta sẽ:

  • Dùng ngân hàng mẫu để phục hồi văn bản từ embedding, ngay cả khi không có huấn luyện trước.
  • Sử dụng Suy luận Thành viên (Membership Inference) để đoán mật khẩu từ danh sách từ, như kiểu thử chìa khóa vào ổ khóa, nhưng thông minh hơn.
  • Phương pháp Tìm kiếm theo Chùm (Beam Search) - giải pháp khi ngân hàng mẫu không đủ tốt.
  • Và cuối cùng, chúng ta sẽ khôi phục thành công mật khẩu N0=Acc3ss từ embedding số - biến số thành chữ, biến vector thành bí mật.

Hãy sẵn sàng đeo kính lúp. Chúng ta sắp mở khóa két sắt.


"Trinh sát không phải là để nhìn thấy kẻ thù. Trinh sát là để nhìn thấy những gì kẻ thù chưa kịp che giấu." — Tôi vừa nghĩ ra câu này, nhưng nó đúng. Hẹn gặp lại ở Phần 3.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí