Linux Shellcode
=====================================================
توقفنا في المقال السابق عند كتابة Shellcode يقوم بطباعة "Hello World" في حالة الـ Linux, وللقيام بذلك سنقوم بكتابته عن طريق الـ Assembly ومن ثم تحويله لـ Shellcode. كما ذكرنا أيضا أنه لابد من استخدام System Call للقيام بمهمة الطباعة.
قبل البدء, لنقم بالمرور سريعا على كيفية عمل System Calls. بداية لمعرفة ما هي الـ System Calls التي يمكننا استخدامها، يمكننا رؤية ذلك داخل الملف الآتي:
سنجد على اليسار جميع الـ System Calls التي يمكننا استخدامها ويقابلها على اليمين الرقم الخاص بكل System Call. هذه الأرقام للتمييز بينها وهي ما سنقوم باستخدامها عند استدعاء أي System Call.
بما أننا ذكرنا سابقا أننا سنقوم بعملية طباعة، إذا سنقوم باستخدام اثنين من الـ System Calls وهما Write و Exit. الأول للقيام بعملية الطباعة والثاني نستخدمه في نهاية أي برنامج ليقوم بإنهاء الـ Process. قد يتساءل البعض عن كيفية عمل هذه الـ System Calls، والإجابة ببساطة باستخدام الأمر man 2 متبوعا باسم الـ Syscall الذي نريد الاستعلام عنه.
مثلا لو أردنا الاستعلام عن كيفية عمل () write فسنستخدم الأمر التالي: man 2 write والذي سيقوم بعرض التالي:
سنجد أن الأمر man 2 write قام بإيضاح المعلومات المطلوبة لاستخدام write () System Call وهي كالآتي:
كما نرى، هناك 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 سنجد أن هناك معلومة واحدة مطلوبة:
وهي الرقم الذي سيوضح الـ 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 أنه يريد منه القيام بمهمة ما.
لنبدأ الآن بكتابة برنامج الـ Assembly الذي سيقوم باستخدام هذه الـ System Calls ومن ثم استخراج الـ Shellcode. ما يلي هو الـ Template التقليدي لأغلب برامج الـ Assembly
كما ذكرنا في الدرس السابق فإن الـ Code البرمجي سيتم وضعه في الـ text section والـ Initialized Variables سيتم وضعها في الـ data section والـ Uninitialized Variables سيتم وضعها في الـ bss section. لنقم الآن بفتح ملف جديد نسميه MyFirstProgram.asm ونضع الكود التالي بداخله:
;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"
كما نرى، الـ Shellcode تم استخراجه عن طريق الأمر objdump. بعدها تأكدنا أن الملف ProgramA أصبح executable قابل للتنفيذ وبعدها قمنا بتشغيله لنرى الـ String تمت طباعته بشكل صحيح ثم في النهاية قمنا بطباعة الـ Status Code الذي قمنا بوضعه في Exit () Syscall خلال كتابة البرنامج.
على الرغم من وجود الـ Shellcode كما هو موضح ولكننا نريد استخراجه منفردا….لذلك سنستخدم الأمر التالي:
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
سنجد الآن أننا استطعنا الحصول على الـ Shellcode بالصورة المتعارف عليها
“\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
/* 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 عن طريق الأمر
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
كما شرحنا سابقا فإن تعليمات الـ 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 مرة أخرى لتطبيق ما ذكرناه.
;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.
جيد...الآن نرى أن البرنامج يعمل كما ينبغي و الـ Shellcode لا يوجد به Null Bytes على الإطلاق كما نلاحظ أيضا أن الـ Size أصبح أصغر وهذا شئ جيد جدا. لنحاول تجربة هذا الـ Shellcode مرة أخرى باستخدام ShellcodeTester.c كما فعلنا سابقا
نلاحظ أن مشكلة الـ 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.
الخلاصة أن الـ 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.
لنطبق هذه الخطوات على "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:
# 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 في البرنامج
;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 من البرنامج
كما نلاحظ الـ Shellcode لا يوجد به أي Null Bytes وأيضا يوجد به الـ String المراد طباعته. الآن لنجرب الـ Shellcode بداخل ShellcodeTester
الآن يمكننا القول بأن الـ Shellcode أصبح Portable ويمكننا استخدامه حيثما أردنا. في الدرس القادم ان شاء الله سنتعرض للطريقة الثانية للتغلب على مشكلة الـ Hard-Coded Addresses وتسمى JMP-CALL-POP كما سنقوم بكتابة Shellcode يوفر Shell Access.