=====================================================
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 والذي ستكون نتيجة تشغيله كالآتي:







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






=====================================================
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 الناتج في كل مرة






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

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




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




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





الخلاصة ، عند كتابة الـ 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 al, 0x4        ;4 = write() syscall number
    xor ebx,ebx        ;clear the register before using it
    mov bl, 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 dl, 24        ;length of "Speak Less, Listen More" + “0xA” = 24 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

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.


Author
Muhammad.Alharmeel
Views
2,147
First release
Last update
Rating
4.93 star(s) 15 ratings

Latest reviews

more of this please !
CooooooL thanks
Greate
جيد جدا
ما شاء الله شرح اكثر من رائع
your explanation made it easy to understand
شرح أكثر من رائع - شكرا للمهندس محمد
جزاك الله بالخير يا مهندس
جزاك الله خيرا
شكرا استاذ
Top