01 - x86 Shellcoding | Linux Shellcode Part 1

How to write linux shellcode

  1. Eng.Nour
    =====================================================
    Linux Shellcode
    =====================================================​


    توقفنا في المقال السابق عند كتابة Shellcode يقوم بطباعة "Hello World" في حالة الـ Linux, وللقيام بذلك سنقوم بكتابته عن طريق الـ Assembly ومن ثم تحويله لـ Shellcode. كما ذكرنا أيضا أنه لابد من استخدام System Call للقيام بمهمة الطباعة.

    قبل البدء, لنقم بالمرور سريعا على كيفية عمل System Calls. بداية لمعرفة ما هي الـ System Calls التي يمكننا استخدامها، يمكننا رؤية ذلك داخل الملف الآتي:




    usr/include/i386-linux-gnu/asm/unistd​

    1-a.png


    سنجد على اليسار جميع الـ System Calls التي يمكننا استخدامها ويقابلها على اليمين الرقم الخاص بكل System Call. هذه الأرقام للتمييز بينها وهي ما سنقوم باستخدامها عند استدعاء أي System Call.

    بما أننا ذكرنا سابقا أننا سنقوم بعملية طباعة، إذا سنقوم باستخدام اثنين من الـ System Calls وهما Write و Exit. الأول للقيام بعملية الطباعة والثاني نستخدمه في نهاية أي برنامج ليقوم بإنهاء الـ Process. قد يتساءل البعض عن كيفية عمل هذه الـ System Calls، والإجابة ببساطة باستخدام الأمر man 2 متبوعا باسم الـ Syscall الذي نريد الاستعلام عنه.

    مثلا لو أردنا الاستعلام عن كيفية عمل () write فسنستخدم الأمر التالي: man 2 write والذي سيقوم بعرض التالي:



    2-a.png

    سنجد أن الأمر man 2 write قام بإيضاح المعلومات المطلوبة لاستخدام write () System Call وهي كالآتي:



    ssize_t write(int fd, const void *buf, size_t count);

    كما نرى، هناك 3 معلومات مطلوبة:

    الأولى: int fd وهي رقم الـ File Descriptor والذي يشير إلى المكان الذي ستتم الطباعة عليه. كما هو معلوم فإن رقم 1 في File Descriptor تعني إطبع على Stdout / الشاشة.

    الثانية: const void *buf وتعني pointer إلى المكان في الذاكرة الذي يحتوي على الـ String المراد طباعته. بمعنى آخر إذا أردنا طباعة "Hello World" فيجب علينا تحميل هذا الـ String في الذاكرة ومعرفة العنوان أو الـ Memory Address الذي يتواجد به ومن ثم استخدامه هنا.

    الثالثة: size_t count وهو الـ Length الخاص بالـ String المراد طباعته.

    والآن لنكرر نفس الخطوات مع Exit () System Call لمعرفة كيف يتم استخدامه. عند استخدام الأمر man 2 exit سنجد أن هناك معلومة واحدة مطلوبة:



    void _Exit(int status);​


    وهي الرقم الذي سيوضح الـ status الخاصة بعملية الـ Exit. يتم استخدام الرقم 0 للدلالة على أن الـ Process قد تم انهائها بطريقة صحيحة وبدون أي مشاكل. كما يتم استخدام أي رقم آخر غير 0 للدلالة على وجود مشكلة.

    بعد أن فهمنا كيفية عمل Write () & Exit () System Calls لم يتبق لنا إلا أن نعرف كيف يتم استدعاء الـ System Call داخل البرنامج. يتم ذلك عن طريق الخطوات التالية:

    -وضع رقم الـ Syscall المراد استدعائه داخل الـ EAX Register

    -وضع الـ Arguments أو المعلومات التي يحتاجها الـ Syscall واحدة تلو الأخرى بالترتيب في EBX,ECX,EDX,ESI,EDI

    -عمل Invoke للـ Syscall (استدعائه) عن طريق الأمر Int 0x80 أو ما يعرف بالـ Interrupt. وهي الطريقة التي يستخدمها البرنامج الذي يعمل في الـ User Space ليخبر الـ Kernel أنه يريد منه القيام بمهمة ما.



    registers before syscall.png


    لنبدأ الآن بكتابة برنامج الـ Assembly الذي سيقوم باستخدام هذه الـ System Calls ومن ثم استخراج الـ Shellcode. ما يلي هو الـ Template التقليدي لأغلب برامج الـ Assembly



    assembly progrm structure.png

    كما ذكرنا في الدرس السابق فإن الـ Code البرمجي سيتم وضعه في الـ text section والـ Initialized Variables سيتم وضعها في الـ data section والـ Uninitialized Variables سيتم وضعها في الـ bss section. لنقم الآن بفتح ملف جديد نسميه MyFirstProgram.asm ونضع الكود التالي بداخله:


    Code:
    ;This is a comment - ProgramA.asm
    
    global _start
    
    section .text
    
    _start:
    
    
        ;Write() Syscall
        mov eax, 0x4        ; 4 = write() syscall number
        mov ebx, 0x1        ; 1 = stdout = print to the screen
        mov ecx, message    ; message is a pointer to the string "Speak Less, Listen More"
        mov edx, 24         ; length of "Speak Less, Listen More" + “0xA” = 24 in decimal
        int 0x80            ; Invoke the syscall
    
        ;Exit() Syscall
        mov eax, 0x1    ; 1 = exit() syscall number
        mov ebx, 0x0    ; 0 = return value. It could be anything, but we picked 0 to indicate clean exit
    
        int 0x80        ; Invoke the syscall
    
    section .data
    
        message: db "Speak Less, Listen More", 0xA    ;declare "message" , assign the string to it and append 0xA to print on new line.
    


    يجب علينا الآن تحويل هذا الـ Source Code إلى برنامج قابل للتنفيذ وبعدها استخراج الـ Shellcode ويتم ذلك عن طريق ثلاث خطوات:

    -عمل Assemble للـ Code عن طريق "nasm -f elf32 -o ProgramA.o ProgramA.asm" لتحويل الـ Assembly إلى Machine Code والاحتفاظ به في ملف يدعى Object File. على الرغم من أن الـ Object File يحتوي على Machine Code ولكنه مازال لا يمكن تنفيذه بصورة مباشرة لوجود Unresolved External References به.

    -عمل Linking عن طريق "ld -o ProgramA ProgramA.o" والذي سيقوم بتحويل الـ Object File من الخطوة السابقة إلى Executable قابل للتنفيذ (ELF File)

    -استخراج الـ Shellcode من الـ Executable عن طريق "Objdump -d ProgramA -M intel"



    1.png

    كما نرى، الـ Shellcode تم استخراجه عن طريق الأمر objdump. بعدها تأكدنا أن الملف ProgramA أصبح executable قابل للتنفيذ وبعدها قمنا بتشغيله لنرى الـ String تمت طباعته بشكل صحيح ثم في النهاية قمنا بطباعة الـ Status Code الذي قمنا بوضعه في Exit () Syscall خلال كتابة البرنامج.

    على الرغم من وجود الـ Shellcode كما هو موضح ولكننا نريد استخراجه منفردا….لذلك سنستخدم الأمر التالي:


    Code:
    for i in `objdump -d ProgramA | tr '\t' ' ' | tr ' ' '\n' | egrep '^[0-9a-f]{2}$' ` ; do echo -n "\x$i" ; done ; echo -e "\n"
    


    ملحوظة: هذا الموقع يحتوي على العديد من الأوامر المفيدة التي تختصر الكثير من المهام عند التعامل مع Bash Scripting




    2.png

    سنجد الآن أننا استطعنا الحصول على الـ Shellcode بالصورة المتعارف عليها



    Code:
    “\xb8\x04\x00\x00\x00\xbb\x01\x00\x00\x00\xb9\xa4\x90\x04\x08\xba\x18\x00\x00\x00\xcd\x80\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80”


    الآن وبعد أن استطعنا الحصول على الـ Shellcode الذي يقوم بطباعة "Speak Less, Listen More"...هل سيعمل بشكل صحيح خلال استخدامه داخل الـ Exploit ؟

    لتجربة هذا الـ Shellcode عمليا بطريقة مماثلة للتي سنستخدمه بها (داخل الـ Exploit) والتأكد من عمله كما ينبغي. يمكننا استخدام الـ Code التالي لتجربة أي Shellcode في حالة الـ Linux



    Code:
    /* ShellcodeTester.c */
    
    #include<stdio.h>
    #include<string.h>
    
    char code[] = "Paste Your Shellcode Here";
    
    int main(int argc, char **argv)
    {
      printf("Shellcode Length:  %d\n", strlen(code));
      int (*func)();
      func = (int (*)()) code;
      (int)(*func)();
    }
    

    بعدها سنقوم بعمل Compile عن طريق الأمر



    Code:
    gcc -fno-stack-protector -z execstack ShellcodeTester.c -o ShellcodeTester
    

    سنجد أن الملف الناتج ELF32 يمكننا تشغيله مباشرة على الـ Linux والذي ستكون نتيجة تشغيله كالآتي:





    [​IMG]

    كما لاحظنا...لم يعمل بشكل صحيح…..فما السبب في ذلك ؟






    =====================================================
    Bad Characters
    =====================================================​



    هي بعض الـ Characters التي إن وجدت بداخل الـ Shellcode فإن الـ Exploit لن تعمل. مثال ذلك 00 أو ما يعرف بـ Null Byte والتي يتم تفسيرها كـ String Terminator والتي تعني تجاهل كل ما يأتي بعدها. فمثلا في الـ Shellcode الذي قمنا باستخراجه في الأعلى ...نلاحظ أن الـ Character الثالث عبارة عن 00 وهو ما يعني أن الجزء المتبقي من الـ Shellcode بعد 00 سيتم تجاهله ولن يتم تنفيذه وبالتالي لن يعمل الـ Shellcode ولن ينفذ المهمة المطلوبة منه.

    تأكيد ذلك أن البرنامج أخبرنا عند تشغيله أن "Shellcode Length: 2" على الرغم من أن الـ Shellcode المستخدم قطعا أكبر من ذلك. إذا فإن الـ Null Byte قامت بتعطيل الـ Shellcode وقام البرنامج بإهمال كل الـ Bytes من بعد x00\

    للتغلب على هذه المشكلة، سنعاود كتابة الـ Shellcode مرة أخرى بحيث لا يحتوي على 00 ولكن لابد لنا من معرفة ما سبب وجود Null Bytes بداخل الـ Shellcode. لنعاود النظر إلى Code الـ Assembly المسئول عن هذه الـ Null Bytes




    3.png

    كما شرحنا سابقا فإن تعليمات الـ Assembly التي على اليمين تمت ترجمتها إلى Machine Code / Shellcode على اليسار ويتضح أمامنا أن الأوامر التي تمت ترجمتها لـ Null Bytes تتضمن كلها نقل/تخزين قيم مختلفة داخل الـ Registers. سبب حدوث ذلك أن السعة التخزينية لكل Register هي 32bit أي 4bytes, لذلك إذا كانت القيمة التي سنقوم بنقلها إلى الـ Register أقل من 4bytes فسيتم ملء الـ bytes الغير مستخدمة من الـ Register بأصفار وهو ما يتسبب في ظهور الـ Null Bytes. للتأكد من ذلك لنقم بتخزين 1byte ثم 2bytes ثم 3bytes ثم 4bytes ونشاهد الـ Shellcode الناتج في كل مرة




    [​IMG]



    سنجد عندما قمنا بتخزين 4bytes لم ينتج أي Null Byte لأننا استخدمنا كل المساحة المتاحة داخل الـ Register وبالتالي تلاشت الحاجة لاستخدام أي Null Byte. الآن بعد أن أدركنا سبب وجود الـ Null Bytes ، كيف نتغلب عليها ؟

    يكمن الحل ببساطة في استخدام جزء فقط من الـ Register حسب حجم الـ Data التي نريد بتخزينها. بمعنى إذا أردنا تخزين 04 في الـ EAX فيمكننا استخدام AL فقط لأن 04 ستقوم بملء 1byte فقط. لذلك يمكننا استخدام mov AL,0x4 والتي ستعطينا نفس النتيجة ولكن باستخدام Shellcode لا يوجد به Null Bytes كما نرى



    [​IMG]


    هذه الطريقة مضمونة باستثناء حالة واحدة. في بعض الأحيان نحتاج إلى تخزين / نقل 0x0 في أحد الـ Registers (كما في حالة Exit Syscall) فحتى لو استخدمنا mov AL,0x0 سنجد أن الـ Shellcode الناتج يوجد به Null Byte



    [​IMG]


    وللتغلب على هذه المشكلة يمكننا استبدال mov AL,0x0 بـ xor eax,eax فمن المعلوم أن ناتج عملية xor لأي Register مع نفسه هو صفر وبذلك نكون وضعنا القيمة 0x0 في الـ Register بطريقة تضمن عدم وجود Null Bytes



    [​IMG]

    الخلاصة ، عند كتابة الـ Shellcode يجب علينا:

    -البدء بعمل Clearing لأي Register سيتم استخدامه عن طريق عملية xor للـ Register مع نفسه لمحو أي قيم سابقة موجودة بداخله

    - تجنب استخدام جزء من الـ Register أكبر من حجم القيمة التي نرغب بتخزينها بداخله عن طريق استخدام AL,BL,CL,DL.

    -استخدام xor في أي وقت نريد فيه تخزين 0x0 بداخل أي Register وتجنب نقل 0x0 مباشرة.

    لنقم الآن بعمل re-write للـ Code مرة أخرى لتطبيق ما ذكرناه.


    Code:
    ;This is a comment - ProgramA-no-nulls.asm
    
    global _start
    
    section .text
    
    _start:
    
        ;Write() Syscall
       xor eax,eax       ;clear the register before using it
       mov eax, 0x4       ;4 = write() syscall number
       xor ebx,ebx       ;clear the register before using it
       mov ebx, 0x1       ;1 = stdout = print to the screen
       mov ecx, message    ;message is a pointer to the string "Speak Less, Listen More"
       xor edx,edx       ;clear the register before using it
       mov edx, 24       ;length of "Speak Less, Listen More" + “0xA” = 24 in decimal
       int 0x80       ;invoke the syscall
    
    
        ;Exit() Syscall
       mov eax, 0x1       ;1 = exit() syscall number - no need to clear because it has been cleared before
       xor ebx,ebx       ;setting EBX to 0 = Exit() status code
       int 0x80             ;invoke the syscall
    
    
    section .data
    
        message: db "Speak Less, Listen More", 0xA    ;declare "message" , assign the string to it and append 0xA to print on new line.
    

    4.png

    جيد...الآن نرى أن البرنامج يعمل كما ينبغي و الـ Shellcode لا يوجد به Null Bytes على الإطلاق كما نلاحظ أيضا أن الـ Size أصبح أصغر وهذا شئ جيد جدا. لنحاول تجربة هذا الـ Shellcode مرة أخرى باستخدام ShellcodeTester.c كما فعلنا سابقا





    5.png

    نلاحظ أن مشكلة الـ Null Bytes لم تعد موجودة حيث استطاع البرنامج قراءة الـ Shellcode بأكمله كما يتضح من Shellcode Length: 25. لكن مع ذلك مازال هناك مشكلة ولم يعمل الـ Shellcode كما هو متوقع ولم يتم طباعة الـ String فما السبب في ذلك ؟

    يرجع ذلك إلى أننا عندما نقوم باستخراج الـ Shellcode من البرنامج ، فإننا نستخرجه من text section فقط (عن طريق objdump -d) وبالتالي لن يحتوي الـ Shellcode على الـ String المراد طباعته لأنه موجود بالـ data section وتكون محصلة ذلك أن الـ Instruction الخامسة (mov ecx,0x804909c) ستصبح بلا فائدة.

    لنشاهد كيف يبدوا شكل برنامج الـ Assembly من الداخل باستخدام (objdump -D بدلا من objdump -d) لعمل disassemble لكل من text section. و data section.




    6.png

    الخلاصة أن الـ Shellcode لم يعمل لأنه لا يحتوي على الـ String المراد طباعته. للتغلب على هذه المشكلة فلابد أن يقوم الـ Shellcode بنفسه بتحميل الـ String في الذاكرة وحساب العنوان الذي يتواجد به الـ String ومن ثم استخدامه داخل الـ Write() Syscall. سنقوم بشرح طريقتين مختلفتين للقيام بذلك.




    =====================================================
    Stack Shellcode
    =====================================================​



    سنقوم بتحميل الـ String في Stack ثم نستخدم الـ Memory Address الذي يشير إلى الـ String ونضعه في ECX. للقيام بذلك يجب علينا:

    -التأكد من أن الـ String Length قابل للقسمة على 4 حتى نستطيع تقسيم الـ String إلى مجموعة من الـ Blocks كل منها 4bytes و سبب ذلك أن السعة التخزينية للـ Stack هي 4bytes. إذا حدث و كان الـ Length غير قابل للقسمة على 4 فيمكننا إضافة Spaces حتي يصبح الـ Length قابل للقسمة على 4.

    -عكس الـ String من اليمين لليسار. عملية العكس هذه سببها أن الـ Stack ينمو بطريقة عكسية ، لذلك نعكس الـ String ليكون شكله النهائي داخل الـ Stack صحيح

    -تحويل الـ String المعكوس من ASCII إلى HEX وتقسيمه إلى Blocks كل واحد 4bytes.

    -عمل PUSH للـ Blocks لتحميل الـ String في الـ Stack والتأكد من وجود Null Byte في الـ Stack في نهاية الـ String لاستخدامها كـ String Terminator.

    الآن أصبح ESP Register الذي يشير إلى قمة الـ Stack في نفس الوقت يشير أيضا إلى الـ String الذي قمنا بتحميله , لذلك سنقوم بنقل الـ ESP إلى ECX وبذلك أصبح الـ ECX يشير إلي الـ Memory Address الذي يحتوي على الـ String.




    9.png

    لنطبق هذه الخطوات على "Speak Less , Listen More\n" , سنجد أن الـ Length = 25bytes ما يعني أننا سنضطر لإضافة ثلاث مسافات في آخر الـ String ليصبح الـ Length =28.

    ملحوظة: n\ في نهاية الـ String لطباعة سطر جديد وهي تفسر كـ 1byte ومكافئها بالـ HEX هو 0x0A


    “Speak Less , Listen More\n "

    -Step 1: Reverse

    “ \neroM netsiL , sseL kaepS”

    -Step 2: Convert to hex

    “2020200a65726f4d206e657473694c202c207373654c206b61657053”

    -Step 3: Divide into blocks

    2020200a
    65726f4d
    206e6574
    73694c20
    2c207373
    654c206b
    61657053

    -Step 4: Push to the stack

    push 0x2020200a
    push 0x65726f4d
    push 0x206e6574
    push 0x73694c20
    push 0x2c207373
    push 0x654c206b
    push 0x61657053

    -Step 5: Copy string address to ECX
    mov ecx,esp


    يمكن استخدام هذا الـ Script لتحويل أي String لمجموعة من الـ Push Instructions جاهزة للاستخدام مباشرة كـ Assembly Code:


    #!/usr/bin/perl
    # Perl script written by Peter Van Eeckhoutte
    # http://www.corelan.be
    # This script takes a string as argument
    # and will produce the opcodes
    # to push this string onto the stack
    #
    if ($#ARGV ne 0) {
    print " usage: $0 ".chr(34)."String to put on stack".chr(34)."\n";
    exit(0);
    }
    #convert string to bytes
    my $strToPush=$ARGV[0];
    my $strThisChar="";
    my $strThisHex="";
    my $cnt=0;
    my $bytecnt=0;
    my $strHex="";
    my $strOpcodes="";
    my $strPush="";
    print "String length : " . length($strToPush)."\n";
    print "Opcodes to push this string onto the stack :\n\n";
    while ($cnt < length($strToPush))
    {
    $strThisChar=substr($strToPush,$cnt,1);
    $strThisHex="\\x".ascii_to_hex($strThisChar);
    if ($bytecnt < 3)
    {
    $strHex=$strHex.$strThisHex;
    $bytecnt=$bytecnt+1;
    }
    else
    {
    $strPush = $strHex.$strThisHex;
    $strPush =~ tr/\\x//d;
    $strHex=chr(34)."\\x68".$strHex.$strThisHex.chr(34).
    " //PUSH 0x".substr($strPush,6,2).substr($strPush,4,2).
    substr($strPush,2,2).substr($strPush,0,2);

    $strOpcodes=$strHex."\n".$strOpcodes;
    $strHex="";
    $bytecnt=0;
    }
    $cnt=$cnt+1;
    }
    #last line
    if (length($strHex) > 0)
    {
    while(length($strHex) < 12)
    {
    $strHex=$strHex."\\x20";
    }
    $strPush = $strHex;
    $strPush =~ tr/\\x//d;
    $strHex=chr(34)."\\x68".$strHex."\\x00".chr(34)." //PUSH 0x00".
    substr($strPush,4,2).substr($strPush,2,2).substr($strPush,0,2);
    $strOpcodes=$strHex."\n".$strOpcodes;
    }
    else
    {
    #add line with spaces + null byte (string terminator)
    $strOpcodes=chr(34)."\\x68\\x20\\x20\\x20\\x00".chr(34).
    " //PUSH 0x00202020"."\n".$strOpcodes;
    }
    print $strOpcodes;


    sub ascii_to_hex ($)
    {
    (my $str = shift) =~ s/(.|\n)/sprintf("%02lx", ord $1)/eg;
    return $str;
    }​

    لنقم الآن بتعديل الـ Code في البرنامج


    Code:
    ;This is a comment - ProgramA-no-nulls-portable-stack.asm
    
    global _start
    
    section .text
    
    _start:
    
    
          ;Write() Syscall
    
           xor eax,eax             ;clear the register before using it
           mov al, 0x4             ;4 = write() syscall number
           xor ebx,ebx             ;clear the register before using it
           push ebx                ;push null byte to the stack to act as string terminator
           mov bl, 0x1             ;1 = stdout = print to the screen
           push 0x2020200a
           push 0x65726f4d
           push 0x206e6574
           push 0x73694c20
           push 0x2c207373
           push 0x654c206b
           push 0x61657053
           mov ecx,esp             ;ECX no contains the address of the string
           xor edx,edx             ;clear the register before using it
           mov dl, 28              ;length of "Speak Less, Listen More\n   " = 28 in decimal
           int 0x80                ;invoke the syscall
    
    
           ;Exit() Syscall
           mov al, 0x1             ;1 = exit() syscall number - no need to clear because it has been cleared before
           xor ebx,ebx             ;setting EBX to 0 = Exit() status code
           int 0x80                ;invoke the syscall
    

    لنستخرج الـ Shellcode من البرنامج




    7.png

    كما نلاحظ الـ Shellcode لا يوجد به أي Null Bytes وأيضا يوجد به الـ String المراد طباعته. الآن لنجرب الـ Shellcode بداخل ShellcodeTester




    8.png



    الآن يمكننا القول بأن الـ Shellcode أصبح Portable ويمكننا استخدامه حيثما أردنا. في الدرس القادم ان شاء الله سنتعرض للطريقة الثانية للتغلب على مشكلة الـ Hard-Coded Addresses وتسمى JMP-CALL-POP كما سنقوم بكتابة Shellcode يوفر Shell Access.


    root_x90, Younes and ahmed harix like this.

Recent Reviews

  1. root_x90
    root_x90
    5/5,
    شرح أكثر من رائع - شكرا للمهندس نور
  2. SaidElbal
    SaidElbal
    5/5,
    جزاك الله بالخير يا مهندس
  3. Younes
    Younes
    5/5,
    جزاك الله خيرا
  4. ahmed harix
    ahmed harix
    5/5,
    شكرا استاذ
  5. Az Mohamed
    Az Mohamed
    4/5,
    I feel really bad when i don't have the ability yet to give back , and see you the only one who give back to the community and help it grow , i wish i can rise faster to follow you lead .
  6. time4x
    time4x
    5/5,
    ربنا يزيدك من العلم وبسم الله طريقه شرح مبسطة جدا
  7. ahmed.meko
    ahmed.meko
    5/5,
    بوركت يا استاذنا كنت في انتظار مقالتك كا العاده