วันอังคารที่ 1 มีนาคม พ.ศ. 2559

Buffer Overflow Attacks

Buffer Overflow Attacks

ผู้เขียน/โดย : เกริก ภิรมย์โสภา (Krerk Piromsopa)
เขียนเมื่อ/ปรับปรุง : 2007-11-24 15:47:24
เนื้อหาในส่วนนี้ รวบรวมขึ้นเพื่อประกอบความเข้าใจเรื่อง Buffer Overflow และ
Buffer-Overflow Attacks เริ่มต้นด้วยความหมายของ Buffer-Overflow จากนั้นเราจะวิเคราะห์ต่อไปว่าปัญหาง่ายๆ
นี้ นำไปสู่ความเสียหายอื่นๆ อันเป็นพื้นฐานของ Computer Viruses หรือ Computer
Worms ได้อย่างไร จากนั้นเราจะวิเคราะห์ปัญหา และลองศึกษาวิธีการป้องกันดู นอกจาก
Buffer-Overflow แล้ว ยังมีปัญหาอื่นๆ คล้ายคลังกันเช่น Integer Overflow หรือ
การใช้ printf ในภาษา C แบบไม่ถูกวิธี อย่างไรก็ตามเราจะไม่กล่าวถึงรายละเอียดปัญหาย่อยๆ
ที่คล้ายคลังกันในที่นี้ เมื่ออ่านจบเนื้อหาในหน้านี้แล้วผู้เขียนหวังเป็นอย่างยิ่งว่า
ผู้อ่านจะมีความเข้าใจในเรื่อง Buffer-Overflow และพัฒนา skill ในการเขียนโปรแกรมมากขึ้น

Buffer Overflow คืออะไร

หลายคนพยายามนิยามหรือให้ความหมายอย่างเป็นทางการของ Buffer Overflow จากการรวบรวมข้อมูลจากหลายๆ
แห่ง แล้ว สรุปใจความได้สั้นๆ ว่า
Buffer Overflow
เกิดขี้นเมื่อมีการเขียน(อ้างอิง)ข้อมูลเกินขอบเขตที่กำหนด
ผลที่เกิดขึ้นคือข้อมูลที่เขียนล้นเข้าไปทับข้อมูลอื่นที่อยู่ในระบบ
ผลที่ตามมาจากการ Overflow ดังกล่าวอาจจะเป็นไปได้ตั้งแต่ทำให้โปรแกรมประมวลผลข้อมูลผิดพลาด,
Segmentation False หรืออาจจะร้ายแรงถึง เปลี่ยนการทำงานของระบบให้ประมวลผล Code
ใดๆ ก็ได้ตามที่ Attackers ต้องการ

Buffer-Overflow Attacks สร้างความเสียหายอะไรได้บ้าง

เพื่อความสะดวกในการอธิบาย จะขอยกตัวอย่างประกอบนะที่นี้โดยเริ่มจากตัวอย่างแบบง่าย
ไปจนถึงการ Attack ที่ซับซ้อนยิ่งขึ้น โดยเริ่มต้นที่ตัวอย่าง Buffer Overflow
แบบง่ายๆ ก่อน แล้วค่อยๆ ยากขึ้นไปจนถึง Buffer-Overflow Attacks ที่ซัพซ้อน (สามารถผ่านระบบป้องกัน
Buffer Overflow บน Software ที่ใช้กันทั่วไป)

ตัวอย่างที่ 1 Buffer-Overflow Attacks แบบเบื้องต้น

ในตัวอย่างแรกนี้ (รูปที่ 1) เป็นตัวอย่างโปรแกรมภาษา C ง่ายๆ ที่เมื่อถูก Overflow
จะทำให้การทำงานบางอย่างผิดไป ในที่นี้ คือตัวแปร age ซึ่งเป็นตัวแปรเก็บตัวเลขจะถูก
Overflow จนค่าที่ได้เปลี่ยนไป ดังในตัวอย่างจะพบว่าอายุเปลี่ยนจาก 15 เป็น 49
(ทั้งที่ไม่ได้ทำการกำหนดค่าตัวแปร age เป็น 49)
#include int main(char argc,char *argv[]) { int age;
char name[8];
char tmp[20];
printf(“Enter your age:”);
gets(tmp);
age=atoi(tmp);
printf(“Enter your name:”);
gets(name); /* 1 */
printf(“———– “);
printf(“%s is %d years old
” ,name,age);
}

$./a.out


Enter your age:15

Enter your name: Krerk.P01

-----------

Krerk.P01 is 49 years old
รูปที่ 1 ตัวอย่าง Buffer-Overflow Attack แบบเบื้องต้น
**** ค่าที่ได้อาจจะแตกต่างกันไป ขึ้นอยู่กับ Processor, ระบบปฏิบัติการ และ ตัว
compiler *****
สิ่งที่เกิดขึ้นคือ คำสั่ง gets(name) (1) ซึ่งรับข้อมูล input ที่มีขนาด 10
ตัว (9 ตัวอักษร “Krerk.P01″ และ 1 terminator) มาเก็บไว้ในตัวแปร name แต่ตัวแปร
name นั้นมีขนาดเพียง 8 ตัวอักษร ทำให้ข้อมูลที่ได้มาเกิด Overflow ไปทับ age ผลที่ตามมาคือตัวแปร
age ซึ่งเดิมเก็บตัวเลข 15 ถูกเปลี่ยนเป็นเก็บตัวอักษร ‘1’ แทน เมื่อ age
ถูกอ้างอิงอีกครั้ง ตัวอักษร ‘1’ จึงถูกอ้างอิงเป็นตัวเลขซึ่งมีค่าเป็น
49 (ในที่นี้ถือตามมาตรฐานรหัส ASCII)

ตัวอย่างที่ 2 Stack Smashing

ในตัวอย่างนี้ จะเป็นทำงานที่ซับซ้อนยิ่งขึ้น โดยก่อนอื่นผู้อ่านจะต้องทำความเข้าใจเรื่องของ
Stack Frame นิดนึงก่อน ทุกครั้งที่โปรแกรมมีการเรียก sub routine ค่า parameters
และ ตำแหน่งการทำงานปัจจุบัน (Return Address) จะถูกเก็บไว้ที่ข้อมูลชั่วคราวคือ
Stack (memory เฉพาะ) หาก sub routine นั้นๆ มีตัวแปรที่เป็น local variable โปรแกรมก็จะจองเนื้อที่บน
Stack เพื่อใช้สำหรับเก็บตัวแปรดังกล่าว การทำงานลักษณะนี้ ทำให้โปรแกรมสามารถอ้างอิง
scope ของ variable ได้ (local variables อ้างอิงจาก stack frame ปัจจุบันเสมอ)
ในรูปที่ 2 ตัว function func ประกอบไปด้วยตัวแปร i, f, ptr, และ
buffer โดยในที่นี้ f เป็น function pointer (สำหรับผู้ที่ไม่คุ้นเคย
funtion pointer เป็น pointer ที่ใช้สำหรับการ dynamic bind ตัวแปร f
ให้เป็น funtion ใดๆ ก็ได้ ในตัวอย่างนี้ เรา bind f ให้เป็น printf) และ
ptr เป็น pointer ทั่วไป และ buffer เป็น array of characters ธรรมดา
เมื่อ func ถูกเรียกใช้งาน ระบบจะทำการสร้าง stack frame
ขึ้นเพื่อจำค่าตำแหน่งปัจจุบัน (ก่อนจะ call subroutine)
ที่โปรแกรมจะต้องกลับมาทำงานต่อ เมื่อเข้ามาสู่โปรแกรม ตัว subroutine
จะบันทึกค่า frame pointer funtion เดิม และจองเนื้อที่สำหรับ local
variables ดังแสดงได้ในด้านขวาของรูปที่ 2
เมื่อวิเคราะห์โปรแกรมดังกล่าว จะพบว่า หากเกิด overflow ที่ตัวแปร
buffer ค่าต่างๆ (อาทิ ptr, f , x, frame pointer, return address)
สามารถถูกเปลี่ยนเป็นค่าใดๆ ก็ได้ เมื่อมี input ที่เหมาะสม
Buffer-overflow attack แบบแรกๆ ที่พบมักจะแทรก code ที่ต้องการให้โปรแกรม
run ลงในตอนต้นของ buffer จากนั้นก็เขียน address ของ buffer
ดังกล่าวต่อท้ายไป ผลที่ได้คือ address ทั้งหลายจะถูกชี้กลับมาที่ buffer
ซึ่งมี code ที่ hacker ต้องการอยู่ เมื่อ address ดังกล่าวถูกอ้างอิง
code เหล่านี้ก็จะถูกประมวลผลโดยปริยาย buffer-overflow attack
ในลักษณะนี้ บางครั้งนิยมเรียกว่า stack smashing
เนื่องจากข้อมูลขนาดใหญ่ถูกปะลงบนเนื้อที่ Stack อย่างไรก็ตาม
บางคนจึงคิดว่าหากเราทำให้ stack ไม่สามารถ execute ได้ (เช่น AMD
non-executable area — NX หรือ patch ที่ Linux kernel บางอัน)
น่าจะสามารถหยุด buffer-overflow attack ได้
int func(char **argv) { int x; int (*f) (const char *, ...);
char *ptr;

char buffer[30];

ptr=buffer;
f=& printf;
f("ptr %p - before ",ptr);
strcpy(ptr,argv[1]);
f(“ptr %p – after “,ptr);
strcpy(ptr,argv[2]);
}
return address
frame pointer
x
f
ptr
buffer
& buffer
& buffer
& buffer
& buffer
& buffer
Malicious Code
รูปที่ 2 ตัวอย่าง Buffer overflow แบบซับซ้อนยิ่งขึ้น
ในความเป็นจริง การ injected code มิใช่สิ่งสำคัญ เพราะหากเราทราบว่ามี
code ที่เราต้องการให้โปรแกรมประมวลผล อยู่ที่ตำแหน่งอันใดอันหนึ่งใน
memory (เช่น code จาก share library หรือตัว code ของโปรแกรมเอง)
เราก็เพียงเปลี่ยนตำแหน่งของ pointers และ addresses ต่างๆ ให้ชี้ไปยัง
code ส่วนนั้น การ attack ในลักษณะนี้ บางคนเรียกว่า arc injection

ตัวอย่างที่ 3 Multi-stage attacks

ในหลายๆ กรณี buffer-overflow attacks จะเกิดขี้นจากการ overflow
หลายครั้งต่อกัน โดยทั่วไปการ overflow ครั้งแรกจะทำให้เกิด pointer
ชี้ไปยังที่ใดก็ได้ในตัวโปรแกรมนั้นๆ และการ overflow
อีกครั้งนึงจะเปลี่ยนค่าที่ pointer ชี้อยู่เป็นค่าที่ต้องการ
ตัวอย่างการ attack ที่พบเช่น Apache mod_SSL SLAPPER Worm ซึ่งการ
overflow ครั้งแรก ช่วยให้เกิด pointer ชี้ไปยัง global offset table
(jump table ที่ใช้อ้างอิงตัวโปรแกรมหลักกับ share library) ของ funtion
free จากนั้น การ overflow ครั้งที่ 2 จะเปลี่ยน entry ดังกล่าวให้มา run
remote shell
ลองพิจารณาโปรแกรมในตัวอย่างที่ 2 อีกครั้งหนึ่ง เราจะพบว่ามี strcpy 2
ครั้ง ในกรณีของ Multi-stage attacks นี้ strcpy ครั้งแรก จะoverflow ตัว
pointer ptr ให้ชี้ไปยังที่ใดก็ได้ของโปรแกรม สุมมุติว่าชี้ไปที่ jump
table ของ printf เมื่อมี strcpy ครั้งที่ 2 เราก็สามารถจะเขียนค่าใดๆ
ก็ได้ที่ entry นั้น ผลที่เกิดขึ้นก็คือ เมื่อเรียก printf ครั้งต่อไป
ตัวโปรมก็จะไปเรียกทำงานค่าที่ระบุแทนโปรแกรม printf ที่อ้างอิงกับ library
นักวิจัยหลายคนเสนอแนวทางการป้องกัน global offset table
โดยให้ทำส่วนดังกล่าวเป็น read only หลังจากที่ได้มีตัวค่าโดย loader
เรียบร้อยแล้ว อย่างไรก็การป้องกันดังกล่าว ก็มิได้ป้องกัน function
pointer หรือส่วนอื่นๆ แต่อย่างใด
ยิ่งไปกว่านั้น หลักการดังกล่าวยังใช้ attacks โปรแกรมที่มีการป้องกัน
Buffer Overflow ด้วยวิธีการที่นำเสนอกันหลายๆ แบบ เช่นการแทรกค่า cannary
เพื่อตรวจสอบว่า address มีการเปลี่ยนแปลงหรือไม่ได้อีกด้วย เช่น
การเปลี่ยนตัว error handling routine ให้เป็นของผู้บุกรุกเอง เป็นต้น
(ดูรายละเอียดเพิ่มเติมได้ที่ Secure Bit 2, และ Buffer Overflow: the Fundamentals และ Secure Bit)

หลักการสำคัญของ Buffer-Overflow

ถึงตรงนี้ บางคนอาจจะตั้งข้อสังเกตุว่า Buffer Overflow
ดูเหมือนจะเป็นปัญหาที่เกิดขึ้นจาก ภาษา C ซึ่งไม่มีระบบ Bound Checking
แต่ในความเป็นจริงคือ ทุกภาษาในปัจจุบัน มักจะถูกแปลงลงมาเป็นภาษาเครื่อง
หรือติดต่อกับระบบปฏิบัติการ ซึ่ง component ต่างๆ มักเขียนในภาษา C (และ
assembly) ตัวอย่างที่เห็นได้ชัดเช่น Buffer-overflow attacks ที่พบใน
Java, Perl, หรือแม้แต่ .Net
เมื่อวิเคราะห์ถึงประเด็นสำคัญที่ทำให้เกิด Buffer-Overflow Attacks
แล้ว เราจะพบว่าปัญหาโดยตรงเกิดจากการที่ Input
ซึ่งเป็นข้อมูลที่นอกเหนือความควบคุมของผู้เขียนโปรแกรม
เข้ามาทำให้เกิดความเสียหายในระบบ จนทำให้การทำงานของโปรแกรมผิดไป [Howard
and LeBlanc] จาก Microsoft กล่าวในหนังสือ Threat Model ว่า “All input
is evil until proven otherwise” และเสนอว่า
ข้อมูลทุกอันต้องมีการตรวจสอบเมื่อมีการส่งผ่านขอบเขตของโปรแกรม (“Data
must be validated as it crosses the boundary between untrusted and
trusted environments.”) ซึ่งข้อคิดนี้ มิใช่แนวคิดใหม่
หากแต่ผู้เขียนโปรแกรมทั่วไปมักมิได้คำนึงถึง
Secure Bit 2 ซึ่งเป็นงานวิจัยของผู้เขียนเอง
เป็นเทคโนโลยีที่จะฝังหลักการดังกล่าวลงในระบบโดยผู้พัฒนาโปรแกรม
ไม่จำเป็นต้องคอยระมัดระวังตรวจสอบ Input ของตัวเองตลอดเวลา
(เพราะแม้นักพัฒนาซอฟต์แวร์มืออาชีพหลายคน
ยังกล่าวว่ามีบ่อยครั้งที่ไม่สามารถตรวจสอบตัวโปรแกรมได้ครบทุกกรณีในขณะพัฒนาตัวโปรแกรม)
รายละเอียดเพิ่มเติมศึกษาได้ที่ Secure Bit 2

บทสรุป

ในโอกาสต่อไป ผู้เขียนจะนำเสนอแนวทางการป้องกัน buffer overflow
แบบต่างๆ ที่นักวิจัยทัวโลกพยายามพัฒนากันออกมา
พร้อมกับชี้ให้เห็นข้อดีข้อเสีย โดยสรุปแล้ว buffer overflow
เป็นปัญหาพื้นฐานของระบบคอมพิวเตอร์ตั้งแต่เรามีคอมพิวเตอร์จนถึงปัจจุบัน
ยิ่งมีคอมพิวเตอร์ต่อไว้กับ internet มากขึ้นเพียงใด ก็มีเป้านิ่งให้
hackers ทั้งหลายได้โจมตีมาขึ้น
ประกอบกับแต่เดิมผู้พัฒนาซอฟต์แวร์รายใหญ่ๆ (อาที MS) เน้นขาย funtioncs
ใช้งาน และ มิได้ใส่ใจกับปัญหาดังกล่าวมากนัก
แม้ปัจจุบันจะมีแนวทางที่เสนอออกมามากมาย
แต่ก็ยังไม่มีแนวทางใดที่สมบูรณ์แบบ ทางแก้ปัญหาที่ดีที่สุด
อาจจะเป็นพยายามฝึกให้
นักพัฒนาซอฟต์แวร์ทั้งหลายใส่ใจกับโปรแกรมที่ตัวเองเขียนมากขึ้น

อ้างอิง/อ่านเพิ่มเติม

  • MSU-CSE-05-9: Secure Bit2 : Transparent, Hardware Buffer-Overflow Protection, Krerk Piromsopa and Richard J. Enbody
  • MSU-CSE-04-47: Buffer Overflow: the Fundamentals, Krerk Piromsopa and Richard J. Enbody
  • MSU-CSE-04-48: Secure Bit : Hardware, Buffer-Overflow Prevention, Krerk Piromsopa and Matthew R. Fletcher and Richard J. Enbody