پروتکل اتصال اشکالزدایی راه دور¶
این پروتکل به ابزارهای خارجی امکان میدهد به یک فرآیند در حال اجرای CPython متصل شوند و کد پایتون را از راه دور اجرا کنند.
پیوستن به یک فرایند پایتون دیگر ممکن است نیازمند مجوزهای اضافی یا پیکربندی باشد، که بستگی به پلتفرم دارد.
غیرفعالسازی اشکالزدایی از راه دور¶
برای غیرفعال کردن پشتیبانی از اشکالزدایی از راه دور، از یکی از موارد زیر استفاده کنید:
متغیر محیطی
PYTHON_DISABLE_REMOTE_DEBUGرا پیش از راهاندازی مفسر روی1تنظیم کنید.از گزینهی خط فرمان
-X disable_remote_debugاستفاده کنید.پایتون را با پرچم ساخت
--without-remote-debugکامپایل کنید.
الزامات دسترسی¶
پیوستن به یک فرایند پایتون در حال اجرا برای اشکالزدایی از راه دور نیازمند پیکربندی خاص در اکثر پلتفرمهاست. نیازمندیهای خاص و مراحل عیبیابی به سیستمعامل شما بستگی دارد:
لینوکس
به طور کلی، شما میتوانید فرایندهای خود را اشکالزدایی کنید، اما چندین پیکربندی رایج وجود دارد که ممکن است این کار را غیرفعال کنند. برخی توزیعهای لینوکس محدودیتهای ptrace را، که به نام «یاما» (Yama) نیز شناخته میشود، به عنوان یک شکل از سختسازی سیستم فعال میکنند. نسخههای اخیر دستور setpriv (util-linux ۲.۴۱، منتشر شده در ژوئن ۲۰۲۵) به شما اجازه میدهند محدودیتهای ptrace را بر اساس هر فرایند شل کنید:
setpriv --ptracer any python3
(این روی فرایندی که در حال اشکالزدایی است پیکربندی شده است.) شما همچنین میتوانید محدودیتهای ptrace را برای تمام فرایندها تا زمان راهاندازی مجدد (reboot) با دستور زیر غیرفعال کنید:
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
این همچنین میتواند به صورت پایدار پیکربندی شود، معمولاً در /etc/sysctl.d.
توجه
غیرفعال کردن ptrace_scope امنیت سیستم را کاهش میدهد و تنها باید در محیطهای کمریسک انجام شود.
این نیز ممکن است که فراخوانی سیستم ptrace به دلیل یک فیلتر امنیتی غیرفعال شده باشد. به طور خاص، این در نسخههای قدیمی برخی نرمافزارهای ظرف رایج بود. داکر ۱۹.۰۳ یا جدیدتر (منتشر شده در ۲۰۱۹) و containerd ۱.۶.۷ یا جدیدتر (منتشر شده در ۲۰۲۲) به طور خودکار استفاده از فراخوانی سیستم ptrace را در داخل ظرفها مجاز میکنند، وقتی روی هسته لینوکس ۴.۸ یا بالاتر اجرا میشوند. اگر شما نمیتوانید به این نسخهها ارتقا دهید، میتوانید ظرف خود را با گزینهای مانند --security-opt seccomp=unconfined بسازید تا فیلتر امنیتی فراخوانی سیستم برای آن ظرف غیرفعال شود. این کار جداسازی ظرف را ضعیف میکند و تنها باید در محیطهای کمریسک انجام شود.
اگر شما نیاز دارید یک فرایندی را که مالک آن نیستید رد کنید، شما به دسترسی سوپرکاربر یا معادل آن نیاز خواهید داشت. این موضوع همچنین بر روی فرایندهایی که اعتبار امنیتی خود را تغییر دادهاند اعمال میشود، مانند فرایندهای set-user-ID یا set-group-ID (هرچند این برای پایتون غیرمعمول است). سعی کنید دستور اشکالزدایی را با sudo -E اجرا کنید.
توجه
قابلیت CAP_SYS_PTRACE معادل دسترسی سوپرکاربر است، به طوری که امکان اشکالزدایی هر فرایندی را فراهم میکند، نه فقط فرآیند خودتان. ممکن است در اینترنت توصیههایی ببینید که استفاده از آن برای دور زدن محدودیتهای ptrace یا فیلترهای فراخوانی سیستم را پیشنهاد میدهند. این کار در عمل ممکن است جواب دهد، همانند sudo، اما این کار به فرآیند اشکالزدایی دسترسی بسیار بیشتری میدهد از آنچه که نیاز دارد و تنها باید در محیطهای کمامنیت انجام شود.
در نهایت، توجه داشته باشید که یک فرآیند در یک زمان تنها میتواند یک ردیاب داشته باشد. اگر قبلاً به یک فرآیند پایتون تحت strace، gdb و غیره متصل شدهید، نمیتوانید به طور همزمان از اشکالزدایی از راه دور استفاده کنید. (دسترسی سوپرکاربر نمیتواند این محدودیت را دور بزند.)
macOS
به طور پیشفرض، macOS امکان اشکالزدایی فرآیندهای دیگر را غیرفعال میکند.
شما میتوانید دودویی پایتون خود را تغییر دهید تا برای اشکالزدایی قابل انتخاب باشد، با دادن یک امضای کد ad-hoc با یک حق دسترسی که امکان اشکالزدایی را فعال میکند. (یک «امضای» ad-hoc تنها یک پیکربندی است بدون هیچ امضای رمزنگاری واقعی یا نیاز به گواهی یا چیز دیگری مانند عضویت در برنامه توسعهدهنده اپل.)
دستورات زیر یک پرونده get-task-allow.plist با حق دسترسی لازم ایجاد کرده و آن را به باینری پایتون اضافه میکنند:
echo '{"com.apple.security.get-task-allow": true}' | plutil -convert xml1 -o get-task-allow.plist -
codesign --sign - --entitlements get-task-allow.plist path/to/bin/python3
که در آن path/to/bin/python3 مسیر باینری پایتون شماست، जिसे میتوانید با اجرای which python3 یا ارزیابی sys.base_executable در REPL پایتون پیدا کنید. (این دستورالعملها برای یک ساخت غیرچارچوب پایتون است. ساختهای چارچوب ممکن است نیاز به پیکربندی متفاوت داشته باشند.)
سپس شما باید بتوانید فرآیندهای پایتون خود را که با آن باینری آغاز شدهاند، اشکالزدایی کنید.
به طور جایگزین، همانند لینوکس، فرآیندهایی با امتیازات سوپرکاربر مانند sudo مشمول این بررسی نیستند و میتوانند فرآیند هر کاربری را در سیستم اشکالزدایی کنند (هرچند بر روی باینریهای خاص، مانند دستورات ارائهشده توسط سیستمعامل، بررسیهای اضافی وجود دارد به دلیل System Integrity Protection).
ویندوز
برای اتصال به یک فرایند دیگر، معمولاً باید ابزار اشکالزدایی خود را با دسترسی مدیریتی اجرا کنید. خط فرمان یا پایانه را بهعنوان مدیر اجرا کنید.
برخی فرایندها ممکن است حتی با حقوق مدیر (Administrator) نیز همچنان غیرقابلدسترس باشند، مگر اینکه امتیاز SeDebugPrivilege را فعال کرده باشید.
برای حل مشکلات دسترسی به پرونده یا پوشه، مجوزهای امنیتی را تنظیم کنید:
روی پرونده یا پوشه راستکلیک کنید و Properties را انتخاب کنید.
برای مشاهده کاربران و گروههای دارای دسترسی، به زبانه Security بروید.
برای تغییر مجوزها، روی Edit کلیک کنید.
حساب کاربری خود را انتخاب کنید.
در مجوزها، خواندن یا کنترل کامل را در صورت نیاز علامت بزنید.
روی Apply کلیک کنید، سپس برای تأیید روی OK کلیک کنید.
توجه
پیش از ادامه، اطمینان حاصل کنید که تمام الزامات دسترسی را برآورده کردهاید.
این بخش پروتکل سطح پایینی را توصیف میکند که به ابزارهای خارجی امکان میدهد یک اسکریپت پایتون را درون یک فرآیند در حال اجرای CPython تزریق و اجرا کنند.
این سازوکار مبنای تابع sys.remote_exec() را تشکیل میدهد، که به یک فرآیند پایتون راه دور دستور میدهد یک پرونده .py را اجرا کند. با این حال، این بخش به مستندسازی نحوهی استفاده از آن تابع نمیپردازد. در عوض، توضیح دقیقی درباره پروتکل زیربنایی ارائه میدهد که بهعنوان ورودی، pid یک فرآیند پایتون هدف و مسیر یک پرونده منبع پایتون که باید اجرا شود را دریافت میکند. این اطلاعات از پیادهسازی مستقل پروتکل، صرفنظر از زبان برنامهنویسی، پشتیبانی میکند.
هشدار
اجرای اسکریپت تزریقشده، به رسیدن مفسر به یک نقطه ارزیابی ایمن وابسته است. در نتیجه، ممکن است اجرا بسته به وضعیت رانتایم فرایند هدف به تأخیر بیفتد.
پس از تزریق، اسکریپت در دفعهی بعد که به یک نقطهی ارزیابی امن رسیده شود، توسط مفسر درون فرایند هدف اجرا میشود. این رویکرد قابلیتهای اجرای از راه دور را بدون تغییر رفتار یا ساختار برنامه پایتون در حال اجرا فراهم میکند.
بخشهای بعدی توصیفی گامبهگام از پروتکل ارائه میدهند، شامل تکنیکهایی برای یافتن ساختارهای مفسر در حافظه، دسترسی ایمن به فیلدهای داخلی و فعالسازی اجرای کد. تفاوتهای خاص پلتفرم در موارد قابل اعمال ذکر شدهاند و پیادهسازیهای نمونه برای روشنسازی هر عملیات گنجانده شدهاند.
یافتن ساختار PyRuntime¶
CPython ساختار PyRuntime را در یک بخش دودویی اختصاصی قرار میدهد تا ابزارهای خارجی بتوانند آن را در زمان رانتایم بیابند. نام و قالب این بخش بر حسب پلتفرم متفاوت است. برای مثال، در سیستمهای ELF از .PyRuntime و در macOS از __DATA,__PyRuntime استفاده میشود. ابزارها میتوانند با بررسی دودویی روی دیسک، آفست این ساختار را بیابند.
ساختار PyRuntime شامل وضعیت سراسری مفسر CPython است و دسترسی به سایر دادههای داخلی، از جمله فهرست مفسرها، وضعیتهای نخ و فیلدهای پشتیبانی اشکالزدا را فراهم میکند.
برای کار با یک فرایند پایتون راهدور، اشکالزدا باید ابتدا نشانی حافظهی ساختار PyRuntime را در فرایند هدف پیدا کند. این نشانی را نمیتوان بهصورت سختکدشده تعیین کرد یا از روی نام نماد محاسبه کرد، زیرا به این بستگی دارد که سیستمعامل پرونده دودویی را کجا بارگذاری کرده است.
روش یافتن PyRuntime به پلتفرم بستگی دارد، اما مراحل بهطور کلی یکسان هستند:
آدرس پایهای را بیابید که دودویی پایتون یا کتابخانه اشتراکی در فرایند هدف در آن بارگذاری شده است.
از پروندهی دودویی روی دیسک برای یافتن آفست بخش
.PyRuntimeاستفاده کنید.برای محاسبهی آدرس در حافظه، آفست بخش را به آدرس پایه اضافه کنید.
بخشهای زیر توضیح میدهند که چگونه این کار را در هر سکوی پشتیبانیشده انجام دهید و شامل کد نمونه هستند.
لینوکس (ELF)
برای یافتن ساختار PyRuntime در لینوکس:
نقشهی حافظهی فرایند را بخوانید (برای مثال،
/proc/<pid>/maps) تا نشانیای را بیابید که پرونده اجرایی پایتون یاlibpythonدر آن بارگذاری شده است.سرآیندهای بخش ELF را در پرونده دودویی تجزیه کنید تا آفست بخش
.PyRuntimeبه دست آید.آن آفست را به آدرس پایهی مرحلهی ۱ اضافه کنید تا آدرس حافظهی
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:
برای دریافت پورت task (task port) از نوع
mach_port_tبرای فرایند هدف،task_for_pid()را فراخوانی کنید. برای خواندن حافظه با استفاده از APIهایی مانندmach_vm_read_overwriteوmach_vm_regionبه این دسته نیاز است.ناحیههای حافظه را پویش کنید تا ناحیهای را که شامل پرونده اجرایی Python یا
libpythonاست بیابید.پرونده دودویی را از دیسک بارگذاری کنید و سرآیندهای 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 در ویندوز:
برای برشماری همه ماژولهای بارگذاریشده در فرایند هدف از ToolHelp API استفاده کنید. این کار با استفاده از توابعی مانند CreateToolhelp32Snapshot، Module32First و Module32Next انجام میشود.
ماژول متناظر با
python.exeیاpythonXY.dllرا شناسایی کنید، که در آنXوYشمارههای اصلی و فرعی نسخه پایتون هستند، و آدرس پایهی آن را ثبت کنید.بخش
PyRuntimرا بیابید. به دلیل محدودیت ۸ نویسهای قالب PE برای نام بخشها (که بهصورتIMAGE_SIZEOF_SHORT_NAMEتعریف شده است)، نام اصلیPyRuntimeکوتاه شده است. این بخش شامل ساختارPyRuntimeاست.آدرس مجازی نسبی بخش (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 متفاوتاند و باید پیش از استفاده بررسی شوند تا از سازگاری آنها اطمینان حاصل شود.
برای خواندن و بررسی آفستهای اشکالزدایی، مراحل زیر را دنبال کنید:
حافظهی فرایند هدف را، شروع از نشانی
PyRuntime، بخوانید، بهگونهای که همان تعداد بایتِ ساختار_Py_DebugOffsetsرا پوشش دهد. این ساختار درست در ابتدای بلوک حافظهیPyRuntimeقرار دارد. چیدمان آن در سرآیندهای داخلی CPython تعریف شده است و در یک نسخهی فرعی مشخص ثابت میماند، اما ممکن است در نسخههای اصلی تغییر کند.بررسی کنید که ساختار حاوی دادههای معتبر باشد:
فیلد
cookieباید با نشانگر اشکالزدایی مورد انتظار مطابقت داشته باشد.فیلد
versionباید با نسخهی مفسر پایتونی که اشکالزدا از آن استفاده میکند، مطابقت داشته باشد.اگر اشکالزدا یا فرایند هدف از یک نسخهی پیشانتشار (برای مثال، آلفا، بتا یا نامزد انتشار) استفاده کند، نسخهها باید دقیقاً با هم مطابقت داشته باشند.
فیلد
free_threadedباید هم در اشکالزدا و هم در فرایند هدف مقدار یکسانی داشته باشد.
اگر ساختار معتبر باشد، میتوان از آفستهای موجود در آن برای مکانیابی فیلدها در حافظه استفاده کرد. اگر هر یک از بررسیها شکست بخورد، اشکالزدا باید عملیات را متوقف کند تا از خواندن حافظه در قالب نادرست جلوگیری شود.
در ادامه یک پیادهسازی نمونه آمده است که _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نشاندهندهی یک نخ در حال اجرا درون یک مفسر است. این شامل وضعیت اجرا و فیلدهای کنترلی است که اشکالزدا از آنها استفاده میکند.
برای پیدا کردن یک نخ:
از آفست
runtime_state.interpreters_headبرای بهدست آوردن نشانی نخستین مفسر در ساختارPyRuntimeاستفاده کنید. این، نقطهی ورود به فهرست پیوندی مفسرهای فعال است.از آفست
interpreter_state.threads_mainبرای دسترسی به وضعیت نخ اصلی مرتبط با مفسر انتخابشده استفاده کنید. این معمولاً قابلاطمینانترین نخ برای هدف قرار دادن است.بهصورت اختیاری، برای پیمایش فهرست پیوندی تمام وضعیتهای نخ، از آفست
interpreter_state.threads_headاستفاده کنید. هر ساختارPyThreadStateشامل یک فیلدnative_thread_idاست که میتوان آن را با یک شناسه نخ هدف مقایسه کرد تا یک نخ خاص پیدا شود.پس از یافتن یک
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) در این فیلد باعث میشود مفسر مکث کند و فعالیت اشکالزدا را بررسی کند.
برای تکمیل تزریق، اشکالزدا باید مراحل زیر را انجام دهد:
مسیر کامل اسکریپت را در بافر
debugger_script_pathبنویسید.debugger_pending_callرا روی1تنظیم کنید.مقدار فعلی
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)
پس از تنظیم این فیلدها، اشکالزدا میتواند فرایند را از سر گیرد (اگر تعلیق شده باشد). مفسر درخواست را در نقطه ارزیابی امن بعدی پردازش میکند، اسکریپت را از دیسک بارگذاری میکند و آن را اجرا میکند.
بر عهدهی اشکالزدا است که اطمینان حاصل کند پرونده اسکریپت در حین اجرا برای فرایند هدف موجود و دسترسپذیر باقی میماند.
توجه
اجرای اسکریپت ناهمگام است. پرونده اسکریپت نمیتواند بلافاصله پس از تزریق حذف شود. اشکالزدا باید پیش از حذف پرونده صبر کند تا اسکریپت تزریقشده اثر مشاهدهپذیری ایجاد کند. این اثر به کاری بستگی دارد که اسکریپت برای انجام آن طراحی شده است. برای مثال، یک اشکالزدا ممکن است پیش از حذف اسکریپت صبر کند تا فرایند راهدور به یک سوکت اتصال برگشتی برقرار کند. هنگامی که چنین اثری مشاهده شد، میتوان با اطمینان فرض کرد که پرونده دیگر مورد نیاز نیست.
خلاصه¶
برای تزریق و اجرای یک اسکریپت پایتون در یک فرایند راهدور:
ساختار
PyRuntimeرا در حافظهی فرایند هدف پیدا کنید.ساختار
_Py_DebugOffsetsرا در ابتدایPyRuntimeبخوانید و اعتبارسنجی کنید.از آفستها برای پیدا کردن یک
PyThreadStateمعتبر استفاده کنید.مسیر یک اسکریپت پایتون را در
debugger_script_pathبنویسید.پرچم
debugger_pending_callرا روی1تنظیم کنید._PY_EVAL_PLEASE_STOP_BITرا در فیلدeval_breakerتنظیم کنید.از سرگیری فرایند (در صورت تعلیق). اسکریپت در نقطهی ارزیابی امن بعدی اجرا خواهد شد.
امنیت و مدل تهدید¶
پروتکل اشکالزدایی از راه دور به همان امکانات اولیهی سیستمعامل متکی است که اشکالزداهای بومی مانند 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 و غیره)، که بر اساس مدلهای مجوز خود در دسترس باقی میمانند.