پروتکل اتصال اشکال‌زدایی راه دور

این پروتکل به ابزارهای خارجی امکان می‌دهد به یک فرآیند در حال اجرای CPython متصل شوند و کد پایتون را از راه دور اجرا کنند.

بیشتر سکوها برای اتصال به یک فرآیند پایتون دیگر به دسترسی‌های ارتقاء‌یافته نیاز دارند.

غیرفعال‌سازی اشکال‌زدایی از راه دور

برای غیرفعال کردن پشتیبانی از اشکال‌زدایی از راه دور، از یکی از موارد زیر استفاده کنید:

الزامات دسترسی

اتصال به یک فرایند پایتون در حال اجرا برای اشکال‌زدایی از راه دور، در بیشتر پلتفرم‌ها به دسترسی‌های ارتقاء‌یافته نیاز دارد. نیازمندی‌های خاص و مراحل عیب‌یابی به سیستم‌عامل شما بستگی دارند:

لینوکس

فرایند ردیاب باید قابلیت CAP_SYS_PTRACE یا امتیازات معادل آن را داشته باشد. شما فقط می‌توانید فرایندهایی را ردگیری کنید که مالک آن‌ها هستید و می‌توانید به آن‌ها سیگنال بفرستید. اگر فرایند از قبل در حال ردگیری باشد، یا با set-user-ID یا set-group-ID اجرا شود، ممکن است ردگیری با شکست مواجه شود. ماژول‌های امنیتی مانند Yama ممکن است ردگیری را بیشتر محدود کنند.

برای کاهش موقت محدودیت‌های ptrace (تا راه‌اندازی مجدد)، اجرا کنید:

echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope

توجه

غیرفعال کردن ptrace_scope مستحکم‌سازی سیستم را کاهش می‌دهد و باید فقط در محیط‌های مورد اعتماد انجام شود.

اگر داخل یک ظرف اجرا می‌شود، از --cap-add=SYS_PTRACE یا --privileged استفاده کنید و در صورت نیاز، به‌عنوان root اجرا کنید.

دستور را دوباره با دسترسی‌های ارتقاء‌یافته اجرا کنید:

sudo -E !!

macOS

برای اتصال به یک فرایند دیگر، معمولاً باید ابزار اشکال‌زدایی خود را با دسترسی‌های بالاتر اجرا کنید. این کار را می‌توان با استفاده از sudo یا اجرا به‌عنوان root انجام داد.

حتی هنگام اتصال به فرایندهایی که متعلق به شما هستند، ممکن است macOS به دلیل محدودیت‌های امنیتی سیستم، اشکال‌زدایی را مسدود کند، مگر اینکه اشکال‌زدا با امتیازات root اجرا شود.

ویندوز

برای اتصال به یک فرایند دیگر، معمولاً باید ابزار اشکال‌زدایی خود را با دسترسی مدیریتی اجرا کنید. خط فرمان یا پایانه را به‌عنوان مدیر اجرا کنید.

برخی فرایندها ممکن است حتی با حقوق مدیر (Administrator) نیز همچنان غیرقابل‌دسترس باشند، مگر اینکه امتیاز SeDebugPrivilege را فعال کرده باشید.

برای حل مشکلات دسترسی به پرونده یا پوشه، مجوزهای امنیتی را تنظیم کنید:

  1. روی پرونده یا پوشه راست‌کلیک کنید و Properties را انتخاب کنید.

  2. برای مشاهده کاربران و گروه‌های دارای دسترسی، به زبانه Security بروید.

  3. برای تغییر مجوزها، روی Edit کلیک کنید.

  4. حساب کاربری خود را انتخاب کنید.

  5. در مجوزها، خواندن یا کنترل کامل را در صورت نیاز علامت بزنید.

  6. روی Apply کلیک کنید، سپس برای تأیید روی OK کلیک کنید.

توجه

پیش از ادامه، اطمینان حاصل کنید که تمام الزامات دسترسی را برآورده کرده‌اید.

این بخش پروتکل سطح پایینی را توصیف می‌کند که به ابزارهای خارجی امکان می‌دهد یک اسکریپت پایتون را درون یک فرآیند در حال اجرای CPython تزریق و اجرا کنند.

این سازوکار مبنای تابع sys.remote_exec() را تشکیل می‌دهد، که به یک فرآیند پایتون راه دور دستور می‌دهد یک پرونده .py را اجرا کند. با این حال، این بخش به مستندسازی نحوه‌ی استفاده از آن تابع نمی‌پردازد. در عوض، توضیح دقیقی درباره پروتکل زیربنایی ارائه می‌دهد که به‌عنوان ورودی، pid یک فرآیند پایتون هدف و مسیر یک پرونده منبع پایتون که باید اجرا شود را دریافت می‌کند. این اطلاعات از پیاده‌سازی مستقل پروتکل، صرف‌نظر از زبان برنامه‌نویسی، پشتیبانی می‌کند.

هشدار

اجرای اسکریپت تزریق‌شده، به رسیدن مفسر به یک نقطه ارزیابی ایمن وابسته است. در نتیجه، ممکن است اجرا بسته به وضعیت ران‌تایم فرایند هدف به تأخیر بیفتد.

پس از تزریق، اسکریپت در دفعه‌ی بعد که به یک نقطه‌ی ارزیابی امن رسیده شود، توسط مفسر درون فرایند هدف اجرا می‌شود. این رویکرد قابلیت‌های اجرای از راه دور را بدون تغییر رفتار یا ساختار برنامه پایتون در حال اجرا فراهم می‌کند.

بخش‌های بعدی توصیفی گام‌به‌گام از پروتکل ارائه می‌دهند، شامل تکنیک‌هایی برای یافتن ساختارهای مفسر در حافظه، دسترسی ایمن به فیلدهای داخلی و فعال‌سازی اجرای کد. تفاوت‌های خاص پلتفرم در موارد قابل اعمال ذکر شده‌اند و پیاده‌سازی‌های نمونه برای روشن‌سازی هر عملیات گنجانده شده‌اند.

یافتن ساختار PyRuntime

CPython ساختار PyRuntime را در یک بخش دودویی اختصاصی قرار می‌دهد تا ابزارهای خارجی بتوانند آن را در زمان ران‌تایم بیابند. نام و قالب این بخش بر حسب پلتفرم متفاوت است. برای مثال، در سیستم‌های ELF از .PyRuntime و در macOS از __DATA,__PyRuntime استفاده می‌شود. ابزارها می‌توانند با بررسی دودویی روی دیسک، آفست این ساختار را بیابند.

ساختار PyRuntime شامل وضعیت سراسری مفسر CPython است و دسترسی به سایر داده‌های داخلی، از جمله فهرست مفسرها، وضعیت‌های نخ و فیلدهای پشتیبانی اشکال‌زدا را فراهم می‌کند.

برای کار با یک فرایند پایتون راه‌دور، اشکال‌زدا باید ابتدا نشانی حافظه‌ی ساختار PyRuntime را در فرایند هدف پیدا کند. این نشانی را نمی‌توان به‌صورت سخت‌کدشده تعیین کرد یا از روی نام نماد محاسبه کرد، زیرا به این بستگی دارد که سیستم‌عامل پرونده دودویی را کجا بارگذاری کرده است.

روش یافتن PyRuntime به پلتفرم بستگی دارد، اما مراحل به‌طور کلی یکسان هستند:

  1. آدرس پایه‌ای را بیابید که دودویی پایتون یا کتابخانه اشتراکی در فرایند هدف در آن بارگذاری شده است.

  2. از پرونده‌ی دودویی روی دیسک برای یافتن آفست بخش .PyRuntime استفاده کنید.

  3. برای محاسبه‌ی آدرس در حافظه، آفست بخش را به آدرس پایه اضافه کنید.

بخش‌های زیر توضیح می‌دهند که چگونه این کار را در هر سکوی پشتیبانی‌شده انجام دهید و شامل کد نمونه هستند.

لینوکس (ELF)

برای یافتن ساختار PyRuntime در لینوکس:

  1. نقشه‌ی حافظه‌ی فرایند را بخوانید (برای مثال، /proc/<pid>/maps) تا نشانی‌ای را بیابید که پرونده اجرایی پایتون یا libpython در آن بارگذاری شده است.

  2. سرآیندهای بخش ELF را در پرونده دودویی تجزیه کنید تا آفست بخش .PyRuntime به دست آید.

  3. آن آفست را به آدرس پایه‌ی مرحله‌ی ۱ اضافه کنید تا آدرس حافظه‌ی PyRuntime به‌دست آید.

در ادامه یک پیاده‌سازی نمونه آمده است:

def find_py_runtime_linux(pid: int) -> int:
    # Step 1: Try to find the Python executable in memory
    binary_path, base_address = find_mapped_binary(
        pid, name_contains="python"
    )

    # Step 2: Fallback to shared library if executable is not found
    if binary_path is None:
        binary_path, base_address = find_mapped_binary(
            pid, name_contains="libpython"
        )

    # Step 3: Parse ELF headers to get .PyRuntime section offset
    section_offset = parse_elf_section_offset(
        binary_path, ".PyRuntime"
    )

    # Step 4: Compute PyRuntime address in memory
    return base_address + section_offset

در سیستم‌های لینوکسی، دو روش اصلی برای خواندن حافظه از یک فرایند دیگر وجود دارد. روش اول از طریق سامانه فایل‌بندی /proc است، به‌ویژه با خواندن از /proc/[pid]/mem که دسترسی مستقیمی به حافظه‌ی فرایند فراهم می‌کند. این کار به مجوزهای مناسب نیاز دارد؛ یا باید همان کاربرِ فرایند هدف باشید یا دسترسی root داشته باشید. روش دوم استفاده از فراخوانی سیستمی process_vm_readv() است که راهی کارآمدتر برای کپی حافظه بین فرایندها فراهم می‌کند. اگرچه می‌توان از عملیات PTRACE_PEEKTEXT در ptrace نیز برای خواندن حافظه استفاده کرد، اما این روش به‌طور قابل‌توجهی کندتر است، زیرا هر بار فقط یک کلمه را می‌خواند و به چندین تعویض زمینه بین فرایندهای ردیاب و ردگیری‌شونده نیاز دارد.

برای تجزیه‌ی بخش‌های ELF، این فرآیند شامل خواندن و تفسیر ساختارهای قالب پرونده ELF از پرونده دودویی روی دیسک است. سرآیند ELF شامل اشاره‌گری به جدول سرآیند بخش‌ها است. هر سرآیند بخش شامل فراداده‌ای درباره‌ی یک بخش است، از جمله نام آن (که در یک جدول رشته‌ای جداگانه ذخیره‌شده است)، آفست و اندازه. برای یافتن بخش خاصی مانند .PyRuntime، باید این سرآیندها را پیمایش کنید و نام بخش را تطبیق دهید. سپس سرآیند بخش آفست محل قرارگیری آن بخش در پرونده را ارائه می‌دهد، که می‌توان از آن برای محاسبه‌ی آدرس ران‌تایم آن هنگام بارگذاری پرونده دودویی در حافظه استفاده کرد.

می‌توانید درباره‌ی قالب پرونده ELF در مشخصات ELF بیشتر بخوانید.

macOS (Mach-O)

برای یافتن ساختار PyRuntime در macOS:

  1. برای دریافت پورت task (task port) از نوع mach_port_t برای فرایند هدف، task_for_pid() را فراخوانی کنید. برای خواندن حافظه با استفاده از APIهایی مانند mach_vm_read_overwrite و mach_vm_region به این دسته نیاز است.

  2. ناحیه‌های حافظه را پویش کنید تا ناحیه‌ای را که شامل پرونده اجرایی Python یا libpython است بیابید.

  3. پرونده دودویی را از دیسک بارگذاری کنید و سرآیند‌های Mach-O را تجزیه کنید تا بخشی به نام PyRuntime در سگمنت __DATA را بیابید. در macOS، به‌طور خودکار یک زیرخط به ابتدای نام نمادها اضافه می‌شود، بنابراین نماد PyRuntime در جدول نمادها به‌صورت _PyRuntime ظاهر می‌شود، اما نام بخش تحت تأثیر قرار نمی‌گیرد.

در ادامه یک پیاده‌سازی نمونه آمده است:

def find_py_runtime_macos(pid: int) -> int:
    # Step 1: Get access to the process's memory
    handle = get_memory_access_handle(pid)

    # Step 2: Try to find the Python executable in memory
    binary_path, base_address = find_mapped_binary(
        handle, name_contains="python"
    )

    # Step 3: Fallback to libpython if the executable is not found
    if binary_path is None:
        binary_path, base_address = find_mapped_binary(
            handle, name_contains="libpython"
        )

    # Step 4: Parse Mach-O headers to get __DATA,__PyRuntime section offset
    section_offset = parse_macho_section_offset(
        binary_path, "__DATA", "__PyRuntime"
    )

    # Step 5: Compute the PyRuntime address in memory
    return base_address + section_offset

در macOS، دسترسی به حافظه‌ی یک فرایند دیگر نیازمند استفاده از APIها و قالب‌های پرونده خاص Mach-O است. نخستین گام، به دست آوردن یک دسته‌ی task_port از طریق task_for_pid() است که دسترسی به فضای حافظه‌ی فرایند هدف را فراهم می‌کند. این دسته عملیات حافظه را از طریق APIهایی مانند mach_vm_read_overwrite() امکان‌پذیر می‌سازد.

می‌توان حافظه‌ی فرایند را با استفاده از mach_vm_region() برای پیمایش فضای حافظه‌ی مجازی بررسی کرد، در حالی که proc_regionfilename() کمک می‌کند تا شناسایی شود کدام پرونده‌های دودویی در هر ناحیه‌ی حافظه بارگذاری شده‌اند. هنگامی که دودویی یا کتابخانه‌ی پایتون پیدا شد، باید سرآیندهای Mach-O آن تجزیه شوند تا محل ساختار PyRuntime پیدا شود.

قالب Mach-O کد و داده را در قالب قطعه‌ها و بخش‌ها سازمان‌دهی می‌کند. ساختار PyRuntime در بخشی به نام __PyRuntime درون قطعه‌ی __DATA قرار دارد. محاسبه‌ی نشانی ران‌تایم واقعی مستلزم پیدا کردن قطعه‌ی __TEXT، که به‌عنوان نشانی پایه‌ی دودویی عمل می‌کند، و سپس یافتن قطعه‌ی __DATA حاوی بخش هدف ما است. نشانی نهایی با ترکیب نشانی پایه با آفست‌های مناسب بخش‌ها از سرآیندهای Mach-O محاسبه می‌شود.

توجه داشته باشید که دسترسی به حافظه‌ی یک فرایند دیگر در macOS معمولاً به امتیازات بالاتری نیاز دارد - یا دسترسی root یا مجوزهای امنیتی خاص (entitlements) که به فرایند اشکال‌زدایی اعطا شده‌اند.

ویندوز (PE)

برای یافتن ساختار PyRuntime در ویندوز:

  1. برای برشماری همه ماژول‌های بارگذاری‌شده در فرایند هدف از ToolHelp API استفاده کنید. این کار با استفاده از توابعی مانند CreateToolhelp32Snapshot، Module32First و Module32Next انجام می‌شود.

  2. ماژول متناظر با python.exe یا pythonXY.dll را شناسایی کنید، که در آن X و Y شماره‌های اصلی و فرعی نسخه پایتون هستند، و آدرس پایه‌ی آن را ثبت کنید.

  3. بخش PyRuntim را بیابید. به دلیل محدودیت ۸ نویسه‌ای قالب PE برای نام بخش‌ها (که به‌صورت IMAGE_SIZEOF_SHORT_NAME تعریف شده است)، نام اصلی PyRuntime کوتاه شده است. این بخش شامل ساختار PyRuntime است.

  4. آدرس مجازی نسبی بخش (RVA) را بازیابی کنید و آن را به آدرس پایه ماژول اضافه کنید.

در ادامه یک پیاده‌سازی نمونه آمده است:

def find_py_runtime_windows(pid: int) -> int:
    # Step 1: Try to find the Python executable in memory
    binary_path, base_address = find_loaded_module(
        pid, name_contains="python"
    )

    # Step 2: Fallback to shared pythonXY.dll if the executable is not
    # found
    if binary_path is None:
        binary_path, base_address = find_loaded_module(
            pid, name_contains="python3"
        )

    # Step 3: Parse PE section headers to get the RVA of the PyRuntime
    # section. The section name appears as "PyRuntim" due to the
    # 8-character limit defined by the PE format (IMAGE_SIZEOF_SHORT_NAME)
    # 8-character limit defined by the PE format (IMAGE_SIZEOF_SHORT_NAME).
    section_rva = parse_pe_section_offset(binary_path, "PyRuntim")

    # Step 4: Compute PyRuntime address in memory
    return base_address + section_rva

در ویندوز، دسترسی به حافظه‌ی یک فرایند دیگر نیازمند استفاده از توابع API ویندوز مانند CreateToolhelp32Snapshot() و Module32First()/Module32Next() برای برشماری ماژول‌های بارگذاری‌شده است. تابع OpenProcess() یک دسته برای دسترسی به فضای حافظه‌ی فرایند هدف فراهم می‌کند و عملیات حافظه را از طریق ReadProcessMemory() ممکن می‌سازد.

حافظه‌ی فرایند را می‌توان با فهرست‌گیری ماژول‌های بارگذاری‌شده برای یافتن دودویی یا DLL پایتون بررسی کرد. هنگامی که پیدا شد، باید سرآیندهای PE آن تجزیه شوند تا ساختار PyRuntime مکان‌یابی شود.

قالب PE، کد و داده را در بخش‌ها سازمان‌دهی می‌کند. ساختار PyRuntime در بخشی به نام "PyRuntim" قرار دارد (که به دلیل محدودیت ۸ نویسه‌ای نام در PE، از "PyRuntime" کوتاه شده است). محاسبه نشانی واقعی ران‌تایم شامل یافتن نشانی پایه ماژول از مدخل ماژول، و سپس یافتن بخش هدف ما در سرآیندهای PE است. نشانی نهایی با ترکیب نشانی پایه با نشانی مجازی بخش، از سرآیندهای بخش در PE محاسبه می‌شود.

توجه داشته باشید که دسترسی به حافظه‌ی فرایندی دیگر در ویندوز معمولاً به امتیازات مناسب نیاز دارد؛ خواه دسترسی مدیریتی، خواه امتیاز SeDebugPrivilege که به فرایند اشکال‌زدایی اعطا شده است.

خواندن _Py_DebugOffsets

پس از تعیین نشانی ساختار PyRuntime، گام بعدی خواندن ساختار _Py_DebugOffsets است که در ابتدای بلوک PyRuntime قرار دارد.

این ساختار، آفست‌های فیلد خاص هر نسخه را فراهم می‌کند که برای خواندن ایمن حافظه‌ی وضعیت مفسر و نخ لازم هستند. این آفست‌ها در نسخه‌های مختلف CPython متفاوت‌اند و باید پیش از استفاده بررسی شوند تا از سازگاری آن‌ها اطمینان حاصل شود.

برای خواندن و بررسی آفست‌های اشکال‌زدایی، مراحل زیر را دنبال کنید:

  1. حافظه‌ی فرایند هدف را، شروع از نشانی PyRuntime، بخوانید، به‌گونه‌ای که همان تعداد بایتِ ساختار _Py_DebugOffsets را پوشش دهد. این ساختار درست در ابتدای بلوک حافظه‌ی PyRuntime قرار دارد. چیدمان آن در سرآیند‌های داخلی CPython تعریف شده است و در یک نسخه‌ی فرعی مشخص ثابت می‌ماند، اما ممکن است در نسخه‌های اصلی تغییر کند.

  2. بررسی کنید که ساختار حاوی داده‌های معتبر باشد:

    • فیلد cookie باید با نشانگر اشکال‌زدایی مورد انتظار مطابقت داشته باشد.

    • فیلد version باید با نسخه‌ی مفسر پایتونی که اشکال‌زدا از آن استفاده می‌کند، مطابقت داشته باشد.

    • اگر اشکال‌زدا یا فرایند هدف از یک نسخه‌ی پیش‌انتشار (برای مثال، آلفا، بتا یا نامزد انتشار) استفاده کند، نسخه‌ها باید دقیقاً با هم مطابقت داشته باشند.

    • فیلد free_threaded باید هم در اشکال‌زدا و هم در فرایند هدف مقدار یکسانی داشته باشد.

  3. اگر ساختار معتبر باشد، می‌توان از آفست‌های موجود در آن برای مکان‌یابی فیلدها در حافظه استفاده کرد. اگر هر یک از بررسی‌ها شکست بخورد، اشکال‌زدا باید عملیات را متوقف کند تا از خواندن حافظه در قالب نادرست جلوگیری شود.

در ادامه یک پیاده‌سازی نمونه آمده است که _Py_DebugOffsets را می‌خواند و بررسی می‌کند:

def read_debug_offsets(pid: int, py_runtime_addr: int) -> DebugOffsets:
    # Step 1: Read memory from the target process at the PyRuntime address
    data = read_process_memory(
        pid, address=py_runtime_addr, size=DEBUG_OFFSETS_SIZE
    )

    # Step 2: Deserialize the raw bytes into a _Py_DebugOffsets structure
    debug_offsets = parse_debug_offsets(data)

    # Step 3: Validate the contents of the structure
    if debug_offsets.cookie != EXPECTED_COOKIE:
        raise RuntimeError("Invalid or missing debug cookie")
    if debug_offsets.version != LOCAL_PYTHON_VERSION:
        raise RuntimeError(
            "Mismatch between caller and target Python versions"
        )
    if debug_offsets.free_threaded != LOCAL_FREE_THREADED:
        raise RuntimeError("Mismatch in free-threaded configuration")

    return debug_offsets

هشدار

تعلیق فرایند توصیه می‌شود

برای اجتناب از شرایط رقابتی و اطمینان از سازگاری حافظه، به‌شدت توصیه می‌شود که فرایند هدف پیش از انجام هر عملیاتی که وضعیت داخلی مفسر را می‌خواند یا می‌نویسد، معلق شود. ران‌تایم پایتون ممکن است در حین اجرای عادی، همزمان ساختارهای داده مفسر را تغییر دهد—مانند ایجاد یا از بین بردن نخ‌ها. این می‌تواند منجر به خواندن یا نوشتن نامعتبر حافظه شود.

یک اشکال‌زدا ممکن است اجرا را با اتصال به فرایند از طریق ptrace یا با ارسال سیگنال SIGSTOP معلق کند. اجرا باید تنها پس از کامل شدن عملیات حافظه‌ای در سمت اشکال‌زدا از سر گرفته شود.

توجه

برخی ابزارها، مانند پروفایل‌گیرها یا اشکال‌زداهای مبتنی بر نمونه‌برداری، ممکن است بدون تعلیق، روی یک فرایند در حال اجرا عمل کنند. در چنین مواردی، ابزارها باید به‌صراحت برای مدیریت حافظه‌ای که به‌طور ناقص به‌روزرسانی شده یا ناسازگار است، طراحی شده باشند. برای بیشتر پیاده‌سازی‌های اشکال‌زدا، تعلیق فرایند همچنان ایمن‌ترین و پایدارترین رویکرد باقی می‌ماند.

یافتن وضعیت مفسر و نخ

پیش از آنکه بتوان کد را در یک فرایند پایتون راه‌دور تزریق و اجرا کرد، اشکال‌زدا باید نخی را انتخاب کند که اجرای کد در آن زمان‌بندی شود. این کار ضروری است، زیرا فیلدهای کنترلی استفاده‌شده برای انجام تزریق کد از راه دور در ساختار _PyRemoteDebuggerSupport قرار دارند که در یک شیء PyThreadState تعبیه شده است. این فیلدها توسط اشکال‌زدا تغییر داده می‌شوند تا اجرای اسکریپت‌های تزریق‌شده درخواست شود.

ساختار PyThreadState یک نخ در حال اجرا درون یک مفسر پایتون را نشان می‌دهد. این ساختار زمینه‌ی ارزیابی نخ را نگه می‌دارد و شامل فیلدهای مورد نیاز برای هماهنگی اشکال‌زدا است. بنابراین، یافتن یک PyThreadState معتبر، پیش‌نیازی کلیدی برای راه‌اندازی اجرا از راه دور است.

یک نخ معمولاً بر اساس نقش یا شناسه آن انتخاب می‌شود. در بیشتر موارد، از نخ اصلی استفاده می‌شود، اما برخی ابزارها ممکن است نخ خاصی را با شناسه نخ بومی آن هدف قرار دهند. پس از انتخاب نخ هدف، اشکال‌زدا باید هم مفسر و هم ساختارهای وضعیت نخ مرتبط را در حافظه پیدا کند.

ساختارهای درونی مرتبط به‌صورت زیر تعریف شده‌اند:

  • PyInterpreterState بیانگر نمونه‌ای مجزا از مفسر پایتون است. هر مفسر مجموعه‌ای از ماژول‌های ایمپورت‌شده، وضعیت توکار و فهرست وضعیت نخ‌های خود را نگه می‌دارد. اگرچه بیشتر برنامه‌های پایتون از یک مفسر واحد استفاده می‌کنند، CPython از چندین مفسر در یک فرایند پشتیبانی می‌کند.

  • PyThreadState نشان‌دهنده‌ی یک نخ در حال اجرا درون یک مفسر است. این شامل وضعیت اجرا و فیلدهای کنترلی است که اشکال‌زدا از آن‌ها استفاده می‌کند.

برای پیدا کردن یک نخ:

  1. از آفست runtime_state.interpreters_head برای به‌دست آوردن نشانی نخستین مفسر در ساختار PyRuntime استفاده کنید. این، نقطه‌ی ورود به فهرست پیوندی مفسرهای فعال است.

  2. از آفست interpreter_state.threads_main برای دسترسی به وضعیت نخ اصلی مرتبط با مفسر انتخاب‌شده استفاده کنید. این معمولاً قابل‌اطمینان‌ترین نخ برای هدف قرار دادن است.

  3. به‌صورت اختیاری، برای پیمایش فهرست پیوندی تمام وضعیت‌های نخ، از آفست interpreter_state.threads_head استفاده کنید. هر ساختار PyThreadState شامل یک فیلد native_thread_id است که می‌توان آن را با یک شناسه نخ هدف مقایسه کرد تا یک نخ خاص پیدا شود.

  4. پس از یافتن یک PyThreadState معتبر، می‌توان از نشانی آن در مراحل بعدی پروتکل استفاده کرد، مانند نوشتن فیلدهای کنترل اشکال‌زدا و زمان‌بندی اجرا.

در ادامه یک پیاده‌سازی نمونه آمده است که وضعیت نخ اصلی را پیدا می‌کند:

def find_main_thread_state(
    pid: int, py_runtime_addr: int, debug_offsets: DebugOffsets,
) -> int:
    # Step 1: Read interpreters_head from PyRuntime
    interp_head_ptr = (
        py_runtime_addr + debug_offsets.runtime_state.interpreters_head
    )
    interp_addr = read_pointer(pid, interp_head_ptr)
    if interp_addr == 0:
        raise RuntimeError("No interpreter found in the target process")

    # Step 2: Read the threads_main pointer from the interpreter
    threads_main_ptr = (
        interp_addr + debug_offsets.interpreter_state.threads_main
    )
    thread_state_addr = read_pointer(pid, threads_main_ptr)
    if thread_state_addr == 0:
        raise RuntimeError("Main thread state is not available")

    return thread_state_addr

مثال زیر نشان می‌دهد که چگونه می‌توان یک نخ را با شناسه نخ بومی آن پیدا کرد:

def find_thread_by_id(
    pid: int,
    interp_addr: int,
    debug_offsets: DebugOffsets,
    target_tid: int,
) -> int:
    # Start at threads_head and walk the linked list
    thread_ptr = read_pointer(
        pid,
        interp_addr + debug_offsets.interpreter_state.threads_head
    )

    while thread_ptr:
        native_tid_ptr = (
            thread_ptr + debug_offsets.thread_state.native_thread_id
        )
        native_tid = read_int(pid, native_tid_ptr)
        if native_tid == target_tid:
            return thread_ptr
        thread_ptr = read_pointer(
            pid,
            thread_ptr + debug_offsets.thread_state.next
        )

    raise RuntimeError("Thread with the given ID was not found")

پس از یافتن یک وضعیت نخ معتبر، اشکال‌زدا می‌تواند به اصلاح فیلدهای کنترلی آن و زمان‌بندی اجرا ادامه دهد، همان‌طور که در بخش بعدی توضیح داده شده است.

نوشتن اطلاعات کنترلی

پس از شناسایی یک ساختار PyThreadState معتبر، اشکال‌زدا می‌تواند فیلدهای کنترلی درون آن را برای زمان‌بندی اجرای یک اسکریپت پایتون مشخص تغییر دهد. این فیلدهای کنترلی به‌طور دوره‌ای توسط مفسر بررسی می‌شوند و هنگامی که به‌درستی تنظیم شوند، باعث اجرای کد راه‌دور در یک نقطه امن در حلقه ارزیابی می‌شوند.

هر PyThreadState شامل یک ساختار _PyRemoteDebuggerSupport است که برای ارتباط بین اشکال‌زدا و مفسر به کار می‌رود. محل‌های فیلدهای آن توسط ساختار _Py_DebugOffsets تعریف شده‌اند و شامل موارد زیر هستند:

  • debugger_script_path: یک بافر با اندازه‌ی ثابت که مسیر کامل یک پرونده منبع پایتون (.py) را در خود نگه می‌دارد. این پرونده باید هنگامی که اجرا راه‌اندازی می‌شود، برای فرایند هدف قابل دسترسی و خواندن باشد.

  • debugger_pending_call: یک پرچم عدد صحیح. تنظیم این مقدار روی 1 به مفسر اطلاع می‌دهد که یک اسکریپت آماده‌ی اجرا است.

  • eval_breaker: فیلدی که مفسر در حین اجرا آن را بررسی می‌کند. تنظیم بیت ۵ (_PY_EVAL_PLEASE_STOP_BIT، مقدار 1U << 5) در این فیلد باعث می‌شود مفسر مکث کند و فعالیت اشکال‌زدا را بررسی کند.

برای تکمیل تزریق، اشکال‌زدا باید مراحل زیر را انجام دهد:

  1. مسیر کامل اسکریپت را در بافر debugger_script_path بنویسید.

  2. debugger_pending_call را روی 1 تنظیم کنید.

  3. مقدار فعلی eval_breaker را بخوانید، بیت ۵ (_PY_EVAL_PLEASE_STOP_BIT) را تنظیم کنید و مقدار به‌روزشده را دوباره بنویسید. این کار به مفسر علامت می‌دهد تا فعالیت اشکال‌زدا را بررسی کند.

در ادامه یک پیاده‌سازی نمونه آمده است:

def inject_script(
    pid: int,
    thread_state_addr: int,
    debug_offsets: DebugOffsets,
    script_path: str
) -> None:
    # Compute the base offset of _PyRemoteDebuggerSupport
    support_base = (
        thread_state_addr +
        debug_offsets.debugger_support.remote_debugger_support
    )

    # Step 1: Write the script path into debugger_script_path
    script_path_ptr = (
        support_base +
        debug_offsets.debugger_support.debugger_script_path
    )
    write_string(pid, script_path_ptr, script_path)

    # Step 2: Set debugger_pending_call to 1
    pending_ptr = (
        support_base +
        debug_offsets.debugger_support.debugger_pending_call
    )
    write_int(pid, pending_ptr, 1)

    # Step 3: Set _PY_EVAL_PLEASE_STOP_BIT (bit 5, value 1 << 5) in
    # eval_breaker
    eval_breaker_ptr = (
        thread_state_addr +
        debug_offsets.debugger_support.eval_breaker
    )
    breaker = read_int(pid, eval_breaker_ptr)
    breaker |= (1 << 5)
    write_int(pid, eval_breaker_ptr, breaker)

پس از تنظیم این فیلدها، اشکال‌زدا می‌تواند فرایند را از سر گیرد (اگر تعلیق شده باشد). مفسر درخواست را در نقطه ارزیابی امن بعدی پردازش می‌کند، اسکریپت را از دیسک بارگذاری می‌کند و آن را اجرا می‌کند.

بر عهده‌ی اشکال‌زدا است که اطمینان حاصل کند پرونده اسکریپت در حین اجرا برای فرایند هدف موجود و دسترس‌پذیر باقی می‌ماند.

توجه

اجرای اسکریپت ناهمگام است. پرونده اسکریپت نمی‌تواند بلافاصله پس از تزریق حذف شود. اشکال‌زدا باید پیش از حذف پرونده صبر کند تا اسکریپت تزریق‌شده اثر مشاهده‌پذیری ایجاد کند. این اثر به کاری بستگی دارد که اسکریپت برای انجام آن طراحی شده است. برای مثال، یک اشکال‌زدا ممکن است پیش از حذف اسکریپت صبر کند تا فرایند راه‌دور به یک سوکت اتصال برگشتی برقرار کند. هنگامی که چنین اثری مشاهده شد، می‌توان با اطمینان فرض کرد که پرونده دیگر مورد نیاز نیست.

خلاصه

برای تزریق و اجرای یک اسکریپت پایتون در یک فرایند راه‌دور:

  1. ساختار PyRuntime را در حافظه‌ی فرایند هدف پیدا کنید.

  2. ساختار _Py_DebugOffsets را در ابتدای PyRuntime بخوانید و اعتبارسنجی کنید.

  3. از آفست‌ها برای پیدا کردن یک PyThreadState معتبر استفاده کنید.

  4. مسیر یک اسکریپت پایتون را در debugger_script_path بنویسید.

  5. پرچم debugger_pending_call را روی 1 تنظیم کنید.

  6. _PY_EVAL_PLEASE_STOP_BIT را در فیلد eval_breaker تنظیم کنید.

  7. از سرگیری فرایند (در صورت تعلیق). اسکریپت در نقطه‌ی ارزیابی امن بعدی اجرا خواهد شد.

امنیت و مدل تهدید

پروتکل اشکال‌زدایی از راه دور به همان امکانات اولیه‌ی سیستم‌عامل متکی است که اشکال‌زداهای بومی مانند GDB و LLDB از آن‌ها استفاده می‌کنند. اتصال به یک فرایند نیازمند همان امتیازها است که آن اشکال‌زداها نیاز دارند، برای مثال ptrace / Yama LSM در لینوکس، task_for_pid در macOS و SeDebugPrivilege در ویندوز. پایتون هیچ مسیر جدیدی برای ارتقاء امتیاز معرفی نمی‌کند؛ اگر مهاجم از قبل دارای دسترسی‌های لازم برای اتصال به یک فرایند باشد، می‌تواند به‌طور مشابه از GDB برای خواندن حافظه یا تزریق کد استفاده کند.

اصول زیر مشخص می‌کنند که چه چیزی در این قابلیت، آسیب‌پذیری امنیتی محسوب می‌شود و چه چیزی محسوب نمی‌شود:

اتصال نیازمند دسترسی‌های سطح سیستم‌عامل است

در همه‌ی پلتفرم‌های پشتیبانی‌شده، سیستم‌عامل دسترسی بین‌فرایندی به حافظه را منوط به بررسی‌های امتیاز می‌کند (CAP_SYS_PTRACE، root یا حقوق مدیر). گزارشی که یک مشکل را تنها پس از آنکه این امتیازها از قبل کسب شده‌اند نشان دهد، یک آسیب‌پذیری در CPython نیست، زیرا از مرز امنیتی سیستم‌عامل از قبل عبور شده است.

فروپاشی‌ها یا خطاهای حافظه هنگام خواندن یک فرایند به‌خطر افتاده، آسیب‌پذیری محسوب نمی‌شوند

ابزاری که وضعیت داخلی مفسر را از یک فرایند هدف می‌خواند، باید فرض کند که آن حافظه خوش‌ساخت است. اگر فرایند هدف تخریب‌شده باشد یا تحت کنترل مهاجم باشد، اشکال‌زدا یا پروفایل‌گیر ممکن است از کار بیفتد، خروجی نامعتبر تولید کند، یا به‌صورت غیرقابل پیش‌بینی رفتار کند. این همان ریسکی است که هر اشکال‌زدای مبتنی بر ptrace آن را می‌پذیرد. اشکال‌های این دسته (سرریزهای بافر، خطاهای قطعه‌بندی (segmentation faults)، یا رفتار تعریف‌نشده ناشی از خواندن وضعیت تخریب‌شده) به‌عنوان مسائل امنیتی تلقی نمی‌شوند، هرچند از اصلاحاتی که استحکام را بهبود می‌بخشند استقبال می‌شود.

آسیب‌پذیری‌های فرایند هدف در محدوده نیستند

اگر فرایند پایتونِ در حال اشکال‌زدایی از قبل به خطر افتاده باشد، مهاجم از قبل کنترل اجرا در آن فرایند را در دست دارد. نشان دادن تأثیر بیشتر از آن نقطه آغاز، آسیب‌پذیری در پروتکل اشکال‌زدایی راه دور محسوب نمی‌شود.

زمان استفاده از PYTHON_DISABLE_REMOTE_DEBUG

متغیر محیطی PYTHON_DISABLE_REMOTE_DEBUG (و پرچم معادل -X disable_remote_debug) به راهبران اجازه می‌دهد سمت درون‌فرایندی پروتکل را به‌عنوان تدبیری برای دفاع در عمق غیرفعال کنند. این ممکن است در محیط‌های استقرار ایمن‌سازی‌شده یا سندباکس‌شده مفید باشد؛ محیط‌هایی که انتظار نمی‌رود هیچ‌گونه اشکال‌زدایی یا پروفایل‌گیری از فرایند در آن‌ها انجام شود و کاهش سطح حمله اولویت دارد، حتی اگر بررسی‌های امتیاز در سطح سیستم‌عامل از پیش از دسترسی فاقد امتیاز جلوگیری کنند.

تنظیم این متغیر بر سایر رابط‌های اشکال‌زدایی در سطح سیستم‌عامل تأثیر نمی‌گذارد (ptrace، /proc، task_for_pid و غیره)، که بر اساس مدل‌های مجوز خود در دسترس باقی می‌مانند.