Giao thức đính kèm gỡ lỗi từ xa¶
Giao thức này cho phép các công cụ bên ngoài gắn vào quy trình CPython đang chạy và thực thi mã Python từ xa.
Hầu hết các nền tảng đều yêu cầu đặc quyền nâng cao để đính kèm vào một quy trình Python khác.
Vô hiệu hóa gỡ lỗi từ xa¶
Để tắt hỗ trợ gỡ lỗi từ xa, hãy sử dụng bất kỳ thao tác nào sau đây:
Đặt biến môi trường
PYTHON_DISABLE_REMOTE_DEBUGthành1trước khi khởi động trình thông dịch.Sử dụng tùy chọn dòng lệnh
-X disable_remote_debug.Biên dịch Python với cờ xây dựng
--without-remote-debug.
Yêu cầu về quyền¶
Việc đính kèm vào quy trình Python đang chạy để gỡ lỗi từ xa yêu cầu đặc quyền nâng cao trên hầu hết các nền tảng. Các yêu cầu cụ thể và các bước khắc phục sự cố tùy thuộc vào hệ điều hành của bạn:
Linux
Quá trình theo dõi phải có khả năng CAP_SYS_PTRACE hoặc các đặc quyền tương đương. Bạn chỉ có thể theo dõi các quy trình bạn sở hữu và có thể báo hiệu. Việc theo dõi có thể không thành công nếu quá trình này đã được theo dõi hoặc nếu nó đang chạy với set-user-ID hoặc set-group-ID. Các mô-đun bảo mật như Yama có thể hạn chế hơn nữa việc truy tìm.
Để tạm thời nới lỏng các hạn chế về ptrace (cho đến khi khởi động lại), hãy chạy:
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
Ghi chú
Việc tắt ptrace_scope giúp giảm độ cứng của hệ thống và chỉ nên thực hiện trong môi trường đáng tin cậy.
Nếu chạy bên trong vùng chứa, hãy sử dụng --cap-add=SYS_PTRACE hoặc --privileged và chạy bằng root nếu cần.
Hãy thử chạy lại lệnh với các đặc quyền nâng cao:
sudo -E !!
macOS
Để đính kèm vào một quy trình khác, bạn thường cần chạy công cụ gỡ lỗi của mình với các đặc quyền nâng cao. Điều này có thể được thực hiện bằng cách sử dụng sudo hoặc chạy bằng root.
Ngay cả khi đính kèm vào các quy trình mà bạn sở hữu, macOS có thể chặn việc gỡ lỗi trừ khi trình gỡ lỗi được chạy với quyền root do các hạn chế về bảo mật hệ thống.
cửa sổ
Để đính kèm vào một quy trình khác, bạn thường cần chạy công cụ gỡ lỗi của mình với các đặc quyền quản trị. Bắt đầu dấu nhắc lệnh hoặc thiết bị đầu cuối với tư cách Quản trị viên.
Một số quy trình có thể vẫn không truy cập được ngay cả với quyền Quản trị viên, trừ khi bạn bật đặc quyền SeDebugPrivilege.
Để giải quyết sự cố truy cập tệp hoặc thư mục, hãy điều chỉnh quyền bảo mật:
Nhấp chuột phải vào tệp hoặc thư mục và chọn Properties.
Chuyển đến tab Security để xem người dùng và nhóm có quyền truy cập.
Nhấp vào Edit để sửa đổi quyền.
Chọn tài khoản người dùng của bạn.
Trong Permissions, hãy kiểm tra Read hoặc Full control nếu cần.
Nhấp vào Apply, sau đó nhấp vào OK để xác nhận.
Ghi chú
Đảm bảo bạn đã hài lòng với tất cả Yêu cầu về quyền trước khi tiếp tục.
Phần này mô tả giao thức cấp thấp cho phép các công cụ bên ngoài chèn và thực thi tập lệnh Python trong quy trình CPython đang chạy.
Cơ chế này tạo thành nền tảng của hàm sys.remote_exec(), hướng dẫn quy trình Python từ xa thực thi tệp .py. Tuy nhiên, phần này không ghi lại cách sử dụng chức năng đó. Thay vào đó, nó cung cấp giải thích chi tiết về giao thức cơ bản, lấy đầu vào là pid của quy trình Python đích và đường dẫn đến tệp nguồn Python sẽ được thực thi. Thông tin này hỗ trợ việc triển khai lại giao thức một cách độc lập, bất kể ngôn ngữ lập trình.
Cảnh báo
Việc thực thi tập lệnh được chèn phụ thuộc vào việc trình thông dịch đạt đến điểm đánh giá an toàn. Kết quả là việc thực thi có thể bị trì hoãn tùy thuộc vào trạng thái thời gian chạy của quy trình đích.
Sau khi được chèn, tập lệnh sẽ được trình thông dịch thực thi trong quy trình đích vào lần tiếp theo đạt đến điểm đánh giá an toàn. Cách tiếp cận này cho phép khả năng thực thi từ xa mà không cần sửa đổi hành vi hoặc cấu trúc của ứng dụng Python đang chạy.
Các phần tiếp theo cung cấp mô tả từng bước về giao thức, bao gồm các kỹ thuật định vị cấu trúc trình thông dịch trong bộ nhớ, truy cập các trường nội bộ một cách an toàn và kích hoạt thực thi mã. Các biến thể dành riêng cho nền tảng được ghi chú khi có thể áp dụng và đưa vào các ví dụ triển khai để làm rõ từng hoạt động.
Định vị cấu trúc PyRuntime¶
CPython đặt cấu trúc PyRuntime trong phần nhị phân chuyên dụng để giúp các công cụ bên ngoài tìm thấy cấu trúc đó trong thời gian chạy. Tên và định dạng của phần này khác nhau tùy theo nền tảng. Ví dụ: .PyRuntime được sử dụng trên hệ thống ELF và __DATA,__PyRuntime được sử dụng trên macOS. Các công cụ có thể tìm thấy phần bù của cấu trúc này bằng cách kiểm tra tệp nhị phân trên đĩa.
Cấu trúc PyRuntime chứa trạng thái trình thông dịch toàn cầu của CPython và cung cấp quyền truy cập vào dữ liệu nội bộ khác, bao gồm danh sách trình thông dịch, trạng thái luồng và các trường hỗ trợ trình gỡ lỗi.
Để làm việc với quy trình Python từ xa, trước tiên trình gỡ lỗi phải tìm địa chỉ bộ nhớ của cấu trúc PyRuntime trong quy trình đích. Địa chỉ này không thể được mã hóa cứng hoặc được tính toán từ tên ký hiệu vì nó phụ thuộc vào vị trí hệ điều hành tải tệp nhị phân.
Phương pháp tìm PyRuntime tùy thuộc vào nền tảng, nhưng nhìn chung các bước đều giống nhau:
Tìm địa chỉ cơ sở nơi thư viện chia sẻ hoặc nhị phân Python được tải trong quy trình đích.
Sử dụng mã nhị phân trên đĩa để xác định phần bù của phần
.PyRuntime.Thêm phần bù vào địa chỉ cơ sở để tính địa chỉ trong bộ nhớ.
Các phần bên dưới giải thích cách thực hiện việc này trên từng nền tảng được hỗ trợ và bao gồm mã ví dụ.
Linux (ELF)
Để tìm cấu trúc PyRuntime trên Linux:
Đọc bản đồ bộ nhớ của quy trình (ví dụ:
/proc/<pid>/maps) để tìm địa chỉ nơi tệp thực thi Python hoặclibpythonđược tải.Phân tích các tiêu đề phần ELF ở dạng nhị phân để lấy phần bù của phần
.PyRuntime.Thêm phần bù đó vào địa chỉ cơ sở từ bước 1 để lấy địa chỉ bộ nhớ của
PyRuntime.
Sau đây là một ví dụ triển khai:
def find_py_runtime_linux(pid: int) -> int:
# Step 1: Cố gắng tìm tệp thực thi Python trong bộ nhớ
đường dẫn nhị phân, base_address = find_mapped_binary(
pid, name_contains="python"
)
# Step 2: Dự phòng vào thư viện dùng chung nếu không tìm thấy tệp thực thi
nếu nhị phân_path là Không có:
đường dẫn nhị phân, base_address = find_mapped_binary(
pid, name_contains="libpython"
)
# Step 3: Phân tích các tiêu đề ELF để lấy phần bù .PyRuntime
phần_offset = phân tích cú pháp_elf_section_offset(
nhị phân_path, ".PyRuntime"
)
# Step 4: Tính địa chỉ PyRuntime trong bộ nhớ
trả về địa chỉ cơ sở + phần_offset
Trên hệ thống Linux, có hai cách tiếp cận chính để đọc bộ nhớ từ một tiến trình khác. Đầu tiên là thông qua hệ thống tập tin /proc, cụ thể là bằng cách đọc từ /proc/[pid]/mem, cung cấp quyền truy cập trực tiếp vào bộ nhớ của tiến trình. Điều này yêu cầu các quyền thích hợp - có cùng người dùng với quy trình đích hoặc có quyền truy cập root. Cách tiếp cận thứ hai là sử dụng lệnh gọi hệ thống process_vm_readv(), cung cấp cách hiệu quả hơn để sao chép bộ nhớ giữa các tiến trình. Mặc dù hoạt động PTRACE_PEEKTEXT của ptrace cũng có thể được sử dụng để đọc bộ nhớ, nhưng nó chậm hơn đáng kể vì nó chỉ đọc một từ mỗi lần và yêu cầu nhiều chuyển đổi ngữ cảnh giữa quá trình theo dõi và theo dõi.
Để phân tích các phần ELF, quy trình này bao gồm việc đọc và giải thích cấu trúc định dạng tệp ELF từ tệp nhị phân trên đĩa. Tiêu đề ELF chứa một con trỏ tới bảng tiêu đề phần. Mỗi tiêu đề phần chứa siêu dữ liệu về một phần bao gồm tên của nó (được lưu trữ trong một bảng chuỗi riêng), độ lệch và kích thước. Để tìm một phần cụ thể như .PyRuntime, bạn cần xem qua các tiêu đề này và khớp với tên phần. Sau đó, tiêu đề phần cung cấp phần bù trong đó phần đó tồn tại trong tệp, phần này có thể được sử dụng để tính địa chỉ thời gian chạy của nó khi tệp nhị phân được tải vào bộ nhớ.
Bạn có thể đọc thêm về định dạng tệp ELF trong ELF specification.
macOS (Mach-O)
Để tìm cấu trúc PyRuntime trên macOS:
Gọi
task_for_pid()để nhận cổng tác vụmach_port_tcho quy trình đích. Tay cầm này cần thiết để đọc bộ nhớ bằng các API nhưmach_vm_read_overwritevàmach_vm_region.Quét các vùng bộ nhớ để tìm vùng chứa tệp thực thi Python hoặc
libpython.Tải tệp nhị phân từ đĩa và phân tích các tiêu đề Mach-O để tìm phần có tên
PyRuntimetrong phân đoạn__DATA. Trên macOS, tên biểu tượng được tự động thêm dấu gạch dưới vào đầu nên biểu tượngPyRuntimexuất hiện dưới dạng_PyRuntimetrong bảng ký hiệu nhưng tên phần không bị ảnh hưởng.
Sau đây là một ví dụ triển khai:
def find_py_runtime_macos(pid: int) -> int:
# Step 1: Truy cập vào bộ nhớ của tiến trình
xử lý = get_memory_access_handle(pid)
# Step 2: Cố gắng tìm tệp thực thi Python trong bộ nhớ
đường dẫn nhị phân, base_address = find_mapped_binary(
xử lý, name_contains="python"
)
# Step 3: Dự phòng về libpython nếu không tìm thấy tệp thực thi
nếu nhị phân_path là Không có:
đường dẫn nhị phân, base_address = find_mapped_binary(
xử lý, name_contains="libpython"
)
# Step 4: Phân tích cú pháp các tiêu đề Mach-O để lấy phần bù phần __DATA,__PyRuntime
phần_offset = phân tích cú pháp_macho_section_offset(
nhị phân_path, "__DATA", "__PyRuntime"
)
# Step 5: Tính địa chỉ PyRuntime trong bộ nhớ
trả về địa chỉ cơ sở + phần_offset
Trên macOS, việc truy cập bộ nhớ của tiến trình khác yêu cầu sử dụng các định dạng tệp và API cụ thể của Mach-O. Bước đầu tiên là lấy bộ điều khiển task_port thông qua task_for_pid(), cung cấp quyền truy cập vào không gian bộ nhớ của tiến trình đích. Tay cầm này cho phép hoạt động bộ nhớ thông qua các API như mach_vm_read_overwrite().
Bộ nhớ xử lý có thể được kiểm tra bằng mach_vm_region() để quét qua không gian bộ nhớ ảo, trong khi proc_regionfilename() giúp xác định tệp nhị phân nào được tải ở mỗi vùng bộ nhớ. Khi tìm thấy thư viện hoặc nhị phân Python, các tiêu đề Mach-O của nó cần được phân tích cú pháp để xác định cấu trúc PyRuntime.
Định dạng Mach-O tổ chức mã và dữ liệu thành các phân đoạn và phần. Cấu trúc PyRuntime nằm trong một phần có tên __PyRuntime trong phân đoạn __DATA. Việc tính toán địa chỉ thời gian chạy thực tế bao gồm việc tìm phân đoạn __TEXT đóng vai trò là địa chỉ cơ sở của tệp nhị phân, sau đó định vị phân đoạn __DATA chứa phần mục tiêu của chúng tôi. Địa chỉ cuối cùng được tính bằng cách kết hợp địa chỉ cơ sở với phần bù phần thích hợp từ các tiêu đề Mach-O.
Lưu ý rằng việc truy cập vào bộ nhớ của tiến trình khác trên macOS thường yêu cầu đặc quyền nâng cao - quyền truy cập root hoặc quyền bảo mật đặc biệt được cấp cho quá trình gỡ lỗi.
Cửa sổ (PE)
Để tìm cấu trúc PyRuntime trên Windows:
Sử dụng ToolHelp API để liệt kê tất cả các mô-đun được tải trong quy trình đích. Điều này được thực hiện bằng cách sử dụng các hàm như CreateToolhelp32Snapshot, Module32First và Module32Next.
Xác định mô-đun tương ứng với
python.exehoặcpythonXY.dll, trong đóXvàYlà số phiên bản chính và phụ của phiên bản Python và ghi lại địa chỉ cơ sở của nó.Xác định vị trí phần
PyRuntim. Do giới hạn 8 ký tự của định dạng PE đối với tên phần (được xác định làIMAGE_SIZEOF_SHORT_NAME), tên ban đầuPyRuntimebị cắt bớt. Phần này chứa cấu trúcPyRuntime.Truy xuất địa chỉ ảo tương đối của phần (RVA) và thêm nó vào địa chỉ cơ sở của mô-đun.
Sau đây là một ví dụ triển khai:
def find_py_runtime_windows(pid: int) -> int:
# Step 1: Cố gắng tìm tệp thực thi Python trong bộ nhớ
đường dẫn nhị phân, base_address = find_loaded_module(
pid, name_contains="python"
)
# Step 2: Dự phòng cho pythonXY.dll được chia sẻ nếu không có tệp thực thi
# found
nếu nhị phân_path là Không có:
đường dẫn nhị phân, base_address = find_loaded_module(
pid, name_contains="python3"
)
# Step 3: Phân tích tiêu đề phần PE để lấy RVA của PyRuntime
# section. Tên phần xuất hiện dưới dạng "PyRuntim" do
giới hạn # 8-character được xác định bởi định dạng PE (IMAGE_SIZEOF_SHORT_NAME).
phần_rva = phân tích cú pháp_pe_section_offset(binary_path, "PyRuntim")
# Step 4: Tính địa chỉ PyRuntime trong bộ nhớ
trả về địa chỉ cơ sở + phần_rva
Trên Windows, việc truy cập bộ nhớ của tiến trình khác yêu cầu sử dụng các hàm API của Windows như CreateToolhelp32Snapshot() và Module32First()/Module32Next() để liệt kê các mô-đun đã tải. Hàm OpenProcess() cung cấp một điều khiển để truy cập vào không gian bộ nhớ của tiến trình đích, cho phép các hoạt động bộ nhớ thông qua ReadProcessMemory().
Bộ nhớ tiến trình có thể được kiểm tra bằng cách liệt kê các mô-đun đã tải để tìm mã nhị phân Python hoặc DLL. Khi được tìm thấy, các tiêu đề PE của nó cần được phân tích cú pháp để xác định cấu trúc PyRuntime.
Định dạng PE tổ chức mã và dữ liệu thành các phần. Cấu trúc PyRuntime nằm trong một phần có tên "PyRuntim" (bị cắt bớt từ "PyRuntime" do giới hạn tên 8 ký tự của PE). Việc tính toán địa chỉ thời gian chạy thực tế bao gồm việc tìm địa chỉ cơ sở của mô-đun từ mục nhập mô-đun, sau đó định vị phần mục tiêu của chúng ta trong các tiêu đề PE. Địa chỉ cuối cùng được tính bằng cách kết hợp địa chỉ cơ sở với địa chỉ ảo của phần từ tiêu đề phần PE.
Lưu ý rằng việc truy cập bộ nhớ của tiến trình khác trên Windows thường yêu cầu các đặc quyền thích hợp - quyền truy cập quản trị hoặc đặc quyền SeDebugPrivilege được cấp cho quá trình gỡ lỗi.
Đọc _Py_DebugOffsets¶
Khi địa chỉ của cấu trúc PyRuntime đã được xác định, bước tiếp theo là đọc cấu trúc _Py_DebugOffsets nằm ở đầu khối PyRuntime.
Cấu trúc này cung cấp độ lệch trường theo phiên bản cụ thể cần thiết để đọc bộ nhớ trạng thái luồng và trình thông dịch một cách an toàn. Các chênh lệch này khác nhau giữa các phiên bản CPython và phải được kiểm tra trước khi sử dụng để đảm bảo chúng tương thích.
Để đọc và kiểm tra bù trừ gỡ lỗi, hãy làm theo các bước sau:
Đọc bộ nhớ từ tiến trình đích bắt đầu từ địa chỉ
PyRuntime, bao gồm cùng số byte như cấu trúc_Py_DebugOffsets. Cấu trúc này nằm ở đầu khối bộ nhớPyRuntime. Bố cục của nó được xác định trong các tiêu đề nội bộ của CPython và giữ nguyên trong một phiên bản nhỏ nhất định nhưng có thể thay đổi trong các phiên bản chính.Kiểm tra xem cấu trúc có chứa dữ liệu hợp lệ không:
Trường
cookiephải khớp với điểm đánh dấu gỡ lỗi dự kiến.Trường
versionphải khớp với phiên bản của trình thông dịch Python được trình gỡ lỗi sử dụng.Nếu trình gỡ lỗi hoặc quy trình đích đang sử dụng phiên bản tiền phát hành (ví dụ: phiên bản alpha, beta hoặc bản phát hành), thì các phiên bản phải khớp chính xác.
Trường
free_threadedphải có cùng giá trị trong cả trình gỡ lỗi và quy trình đích.
Nếu cấu trúc hợp lệ, các offset mà nó chứa có thể được sử dụng để định vị các trường trong bộ nhớ. Nếu bất kỳ kiểm tra nào không thành công, trình gỡ lỗi sẽ dừng hoạt động để tránh đọc bộ nhớ ở định dạng sai.
Sau đây là ví dụ triển khai đọc và kiểm tra _Py_DebugOffsets:
def read_debug_offsets(pid: int, py_runtime_addr: int) -> DebugOffsets:
# Step 1: Đọc bộ nhớ từ tiến trình đích tại địa chỉ PyRuntime
dữ liệu = read_process_memory(
pid, địa chỉ=py_runtime_addr, size=DEBUG_OFFSETS_SIZE
)
# Step 2: Giải tuần tự hóa các byte thô thành cấu trúc _Py_DebugOffsets
debug_offsets=parse_debug_offsets(dữ liệu)
# Step 3: Xác thực nội dung của cấu trúc
nếu debug_offsets.cookie != EXPECTED_COOKIE:
raise RuntimeError("Cookie gỡ lỗi không hợp lệ hoặc bị thiếu")
nếu debug_offsets.version != LOCAL_PYTHON_VERSION:
tăng RuntimeError(
"Không khớp giữa phiên bản Python của người gọi và đích"
)
nếu debug_offsets.free_threaded != LOCAL_FREE_THREADED:
raise RuntimeError("Không khớp trong cấu hình luồng tự do")
trả về debug_offsets
Cảnh báo
Process suspension recommended
Để tránh tình trạng dồn đuổi và đảm bảo tính nhất quán của bộ nhớ, chúng tôi đặc biệt khuyên bạn nên tạm dừng quy trình đích trước khi thực hiện bất kỳ thao tác nào đọc hoặc ghi trạng thái trình thông dịch nội bộ. Thời gian chạy Python có thể đồng thời thay đổi cấu trúc dữ liệu của trình thông dịch—chẳng hạn như tạo hoặc hủy các luồng—trong quá trình thực thi thông thường. Điều này có thể dẫn đến việc đọc hoặc ghi bộ nhớ không hợp lệ.
Trình gỡ lỗi có thể tạm dừng thực thi bằng cách gắn vào quy trình với ptrace hoặc bằng cách gửi tín hiệu SIGSTOP. Việc thực thi chỉ nên được tiếp tục sau khi các thao tác bộ nhớ phía trình gỡ lỗi hoàn tất.
Ghi chú
Một số công cụ, chẳng hạn như trình lược tả hoặc trình gỡ lỗi dựa trên lấy mẫu, có thể hoạt động trên một quy trình đang chạy mà không bị tạm dừng. Trong những trường hợp như vậy, các công cụ phải được thiết kế rõ ràng để xử lý bộ nhớ được cập nhật một phần hoặc không nhất quán. Đối với hầu hết việc triển khai trình gỡ lỗi, việc tạm dừng quy trình vẫn là cách tiếp cận an toàn và mạnh mẽ nhất.
Định vị trình thông dịch và trạng thái luồng¶
Trước khi mã có thể được chèn và thực thi trong quy trình Python từ xa, trình gỡ lỗi phải chọn một luồng để lên lịch thực thi. Điều này là cần thiết vì các trường điều khiển được sử dụng để thực hiện việc chèn mã từ xa nằm trong cấu trúc _PyRemoteDebuggerSupport, được nhúng trong đối tượng PyThreadState. Các trường này được trình gỡ lỗi sửa đổi để yêu cầu thực thi các tập lệnh được chèn.
Cấu trúc PyThreadState đại diện cho một luồng chạy bên trong trình thông dịch Python. Nó duy trì bối cảnh đánh giá của luồng và chứa các trường cần thiết để phối hợp trình gỡ lỗi. Do đó, việc xác định vị trí PyThreadState hợp lệ là điều kiện tiên quyết quan trọng để kích hoạt thực thi từ xa.
Một luồng thường được chọn dựa trên vai trò hoặc ID của nó. Trong hầu hết các trường hợp, luồng chính được sử dụng, nhưng một số công cụ có thể nhắm mục tiêu một luồng cụ thể theo ID luồng gốc của nó. Khi luồng đích được chọn, trình gỡ lỗi phải định vị cả trình thông dịch và cấu trúc trạng thái luồng liên quan trong bộ nhớ.
Các cấu trúc bên trong có liên quan được xác định như sau:
PyInterpreterStateđại diện cho một phiên bản trình thông dịch Python bị cô lập. Mỗi trình thông dịch duy trì tập hợp các mô-đun đã nhập, trạng thái tích hợp và danh sách trạng thái luồng của riêng mình. Mặc dù hầu hết các ứng dụng Python sử dụng một trình thông dịch duy nhất nhưng CPython hỗ trợ nhiều trình thông dịch trong cùng một quy trình.PyThreadStateđại diện cho một luồng chạy trong trình thông dịch. Nó chứa trạng thái thực thi và các trường điều khiển được trình gỡ lỗi sử dụng.
Để xác định vị trí một chủ đề:
Sử dụng offset
runtime_state.interpreters_headđể lấy địa chỉ của trình thông dịch đầu tiên trong cấu trúcPyRuntime. Đây là điểm vào danh sách liên kết của các thông dịch viên đang hoạt động.Sử dụng offset
interpreter_state.threads_mainđể truy cập trạng thái luồng chính được liên kết với trình thông dịch đã chọn. Đây thường là chủ đề đáng tin cậy nhất để nhắm mục tiêu.Theo tùy chọn, hãy sử dụng offset
interpreter_state.threads_headđể lặp qua danh sách được liên kết của tất cả các trạng thái luồng. Mỗi cấu trúcPyThreadStatechứa trườngnative_thread_id, trường này có thể được so sánh với ID luồng đích để tìm một luồng cụ thể.Sau khi tìm thấy
PyThreadStatehợp lệ, địa chỉ của nó có thể được sử dụng trong các bước sau của giao thức, chẳng hạn như ghi các trường điều khiển trình gỡ lỗi và lập lịch thực thi.
Sau đây là ví dụ triển khai để xác định trạng thái của luồng chính:
chắc chắn find_main_thread_state(
pid: int, py_runtime_addr: int, debug_offsets: DebugOffsets,
) -> int:
# Step 1: Đọc thông dịch viên_head từ PyRuntime
interp_head_ptr = (
py_runtime_addr + debug_offsets.runtime_state.interpreters_head
)
interp_addr = read_pointer(pid, interp_head_ptr)
nếu interp_addr == 0:
raise RuntimeError("Không tìm thấy trình thông dịch nào trong tiến trình đích")
# Step 2: Đọc con trỏ thread_main từ trình thông dịch
chủ đề_main_ptr = (
interp_addr + debug_offsets.interpreter_state.threads_main
)
thread_state_addr = read_pointer(pid, thread_main_ptr)
nếu thread_state_addr == 0:
raise RuntimeError("Trạng thái luồng chính không khả dụng")
trả về thread_state_addr
Ví dụ sau đây minh họa cách định vị một luồng bằng ID luồng gốc của nó:
chắc chắn find_thread_by_id(
pid: int,
interp_addr: int,
debug_offsets: DebugOffsets,
target_tid: int,
) -> int:
# Start tại thread_head và xem danh sách liên kết
thread_ptr = read_pointer(
pid,
interp_addr + debug_offsets.interpreter_state.threads_head
)
trong khi thread_ptr:
bản địa_tid_ptr = (
thread_ptr + debug_offsets.thread_state.native_thread_id
)
bản địa_tid = read_int(pid, bản địa_tid_ptr)
nếu Native_tid == target_tid:
trả về thread_ptr
thread_ptr = read_pointer(
pid,
thread_ptr + debug_offsets.thread_state.next
)
raise RuntimeError("Không tìm thấy chủ đề có ID đã cho")
Khi trạng thái luồng hợp lệ đã được xác định, trình gỡ lỗi có thể tiến hành sửa đổi các trường điều khiển của nó và lập lịch thực thi, như được mô tả trong phần tiếp theo.
Viết thông tin điều khiển¶
Sau khi xác định được cấu trúc PyThreadState hợp lệ, trình gỡ lỗi có thể sửa đổi các trường điều khiển bên trong cấu trúc đó để lên lịch thực thi tập lệnh Python được chỉ định. Các trường điều khiển này được trình thông dịch kiểm tra định kỳ và khi được đặt chính xác, chúng sẽ kích hoạt việc thực thi mã từ xa tại một điểm an toàn trong vòng đánh giá.
Mỗi PyThreadState chứa cấu trúc _PyRemoteDebuggerSupport được sử dụng để liên lạc giữa trình gỡ lỗi và trình thông dịch. Vị trí của các trường của nó được xác định bởi cấu trúc _Py_DebugOffsets và bao gồm:
debugger_script_path: Bộ đệm có kích thước cố định chứa đường dẫn đầy đủ đến tệp nguồn Python (.py). Tệp này phải có thể truy cập và đọc được bởi tiến trình đích khi quá trình thực thi được kích hoạt.debugger_pending_call: Cờ số nguyên. Đặt giá trị này thành1sẽ cho trình thông dịch biết rằng tập lệnh đã sẵn sàng để thực thi.eval_breaker: Trường được trình thông dịch kiểm tra trong khi thực thi. Đặt bit 5 (_PY_EVAL_PLEASE_STOP_BIT, giá trị1U << 5) trong trường này sẽ khiến trình thông dịch tạm dừng và kiểm tra hoạt động của trình gỡ lỗi.
Để hoàn tất quá trình chèn, trình gỡ lỗi phải thực hiện các bước sau:
Viết đường dẫn tập lệnh đầy đủ vào bộ đệm
debugger_script_path.Đặt
debugger_pending_callthành1.Đọc giá trị hiện tại của
eval_breaker, đặt bit 5 (_PY_EVAL_PLEASE_STOP_BIT) và ghi lại giá trị đã cập nhật. Điều này báo hiệu cho trình thông dịch kiểm tra hoạt động của trình gỡ lỗi.
Sau đây là một ví dụ triển khai:
def tiêm_script(
pid: int,
thread_state_addr: int,
debug_offsets: DebugOffsets,
script_path: str
) -> Không có:
# Compute phần bù cơ sở của _PyRemoteDebuggerSupport
hỗ trợ_base = (
thread_state_addr +
debug_offsets.debugger_support.remote_debugger_support
)
# Step 1: Ghi đường dẫn script vào debugger_script_path
script_path_ptr = (
hỗ trợ_base +
debug_offsets.debugger_support.debugger_script_path
)
write_string(pid, script_path_ptr, script_path)
# Step 2: Đặt debugger_pending_call thành 1
đang chờ xử lý_ptr = (
hỗ trợ_base +
debug_offsets.debugger_support.debugger_pending_call
)
write_int(pid, đang chờ xử lý_ptr, 1)
# Step 3: Đặt _PY_EVAL_PLEASE_STOP_BIT (bit 5, giá trị 1 << 5) trong
# eval_breaker
eval_breaker_ptr = (
thread_state_addr +
debug_offsets.debugger_support.eval_breaker
)
bộ ngắt = read_int(pid, eval_breaker_ptr)
bộ ngắt |= (1 << 5)
write_int(pid, eval_breaker_ptr, bộ ngắt)
Khi các trường này được đặt, trình gỡ lỗi có thể tiếp tục quá trình (nếu nó bị tạm dừng). Trình thông dịch sẽ xử lý yêu cầu tại điểm đánh giá an toàn tiếp theo, tải tập lệnh từ đĩa và thực thi nó.
Trách nhiệm của trình gỡ lỗi là đảm bảo rằng tệp tập lệnh vẫn hiện diện và có thể truy cập được đối với quy trình đích trong khi thực thi.
Ghi chú
Việc thực thi tập lệnh không đồng bộ. Không thể xóa tập lệnh ngay sau khi tiêm. Trình gỡ lỗi phải đợi cho đến khi tập lệnh được chèn tạo ra hiệu ứng có thể quan sát được trước khi xóa tệp. Hiệu ứng này phụ thuộc vào mục đích của kịch bản. Ví dụ: trình gỡ lỗi có thể đợi cho đến khi quy trình từ xa kết nối trở lại ổ cắm trước khi xóa tập lệnh. Khi quan sát thấy hiệu ứng như vậy, có thể an toàn khi cho rằng tệp không còn cần thiết nữa.
Tóm tắt¶
Để chèn và thực thi tập lệnh Python trong một quy trình từ xa:
Xác định cấu trúc
PyRuntimetrong bộ nhớ của tiến trình đích.Đọc và xác thực cấu trúc
_Py_DebugOffsetsở đầuPyRuntime.Sử dụng độ lệch để xác định vị trí
PyThreadStatehợp lệ.Viết đường dẫn đến tập lệnh Python vào
debugger_script_path.Đặt cờ
debugger_pending_callthành1.Đặt
_PY_EVAL_PLEASE_STOP_BITtrong trườngeval_breaker.Tiếp tục quá trình (nếu bị đình chỉ). Tập lệnh sẽ thực thi tại điểm đánh giá an toàn tiếp theo.
Mô hình an ninh và mối đe dọa¶
Giao thức gỡ lỗi từ xa dựa trên cùng các nguyên tắc cơ bản của hệ điều hành được sử dụng bởi các trình gỡ lỗi gốc như GDB và LLDB. Việc đính kèm vào một quy trình yêu cầu same privileges mà các trình gỡ lỗi đó yêu cầu, ví dụ: ptrace / Yama LSM trên Linux, task_for_pid trên macOS và SeDebugPrivilege trên Windows. Python không giới thiệu bất kỳ đường dẫn leo thang đặc quyền mới nào; nếu kẻ tấn công đã có các quyền cần thiết để đính kèm vào một quy trình, chúng cũng có thể sử dụng GDB để đọc bộ nhớ hoặc tiêm mã.
Các nguyên tắc sau đây xác định điều gì được và không được coi là lỗ hổng bảo mật trong tính năng này:
- Việc đính kèm yêu cầu đặc quyền cấp hệ điều hành
Trên mọi nền tảng được hỗ trợ, cổng hệ điều hành truy cập bộ nhớ xử lý chéo đằng sau các kiểm tra đặc quyền (quyền
CAP_SYS_PTRACE, root hoặc quản trị viên). Một báo cáo chỉ cho thấy sự cố xảy ra sau khi đã có được các đặc quyền này là not là một lỗ hổng trong CPython do ranh giới bảo mật của hệ điều hành đã bị vượt qua.- Sự cố hoặc lỗi bộ nhớ khi đọc quy trình bị xâm nhập không phải là lỗ hổng
Một công cụ đọc trạng thái trình thông dịch nội bộ từ một quy trình đích phải tin tưởng rằng bộ nhớ đó được định dạng đúng. Nếu quy trình đích bị hỏng hoặc bị kẻ tấn công kiểm soát, trình gỡ lỗi hoặc trình lược tả có thể gặp sự cố, tạo ra kết quả rác hoặc hoạt động không thể đoán trước. Đây là rủi ro tương tự được chấp nhận bởi mọi trình gỡ lỗi dựa trên
ptrace. Các lỗi trong danh mục này (tràn bộ đệm, lỗi phân đoạn hoặc hành vi không xác định được kích hoạt bằng cách đọc trạng thái bị hỏng) được not coi là sự cố bảo mật, mặc dù các bản sửa lỗi giúp cải thiện độ mạnh mẽ vẫn được hoan nghênh.- Các lỗ hổng trong quy trình mục tiêu không nằm trong phạm vi
Nếu quy trình Python đang được gỡ lỗi đã bị xâm phạm thì kẻ tấn công đã kiểm soát việc thực thi trong quy trình đó. Việc thể hiện tác động sâu hơn từ điểm xuất phát đó không tạo thành lỗ hổng trong giao thức gỡ lỗi từ xa.
Khi nào nên sử dụng PYTHON_DISABLE_REMOTE_DEBUG¶
Biến môi trường PYTHON_DISABLE_REMOTE_DEBUG (và cờ -X disable_remote_debug tương đương) cho phép người vận hành vô hiệu hóa phần đang xử lý của giao thức dưới dạng thước đo defence-in-depth. Điều này có thể hữu ích trong các môi trường triển khai cứng hoặc hộp cát, nơi không cần gỡ lỗi hoặc lập hồ sơ quy trình và việc giảm bề mặt tấn công là ưu tiên hàng đầu, mặc dù việc kiểm tra đặc quyền cấp hệ điều hành đã ngăn chặn quyền truy cập không có đặc quyền.
Việc đặt biến này sẽ khiến not ảnh hưởng đến các giao diện gỡ lỗi cấp hệ điều hành khác (ptrace, /proc, task_for_pid, v.v.), những giao diện này vẫn khả dụng theo mô hình cấp phép riêng của chúng.